diff --git a/.github/actions/database-migration-image/Dockerfile b/.github/actions/database-migration-image/Dockerfile index 3d704275a3..285b4f65cb 100644 --- a/.github/actions/database-migration-image/Dockerfile +++ b/.github/actions/database-migration-image/Dockerfile @@ -10,6 +10,8 @@ COPY cda-user.sql /after.install.d/ ENV RDS_MODE="true" ENV BUILDUSER=DBAdmin +# Due to the way we originally set this up it needs to always be 99.99.99-SNAPSHOT for now. +RUN sed -i "s/.*<\/revision>/99.99.99-SNAPSHOT<\/revision>/" ../pom.xml ENTRYPOINT [ "/entry.sh" ] CMD ["/cwmsdb/schema/docker/install.sh"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39c352f5aa..702ea65629 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,8 @@ jobs: image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:latest-dev" - env: release image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:25.07.01" + - env: next-release + image: "ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:26.02.17-RC01" name: build and test (jdk ${{matrix.jdk}}, schema ${{matrix.schema.env}}) runs-on: ubuntu-latest outputs: @@ -32,7 +34,7 @@ jobs: - name: checkout code uses: actions/checkout@v5.0.0 - name: setup java - uses: actions/setup-java@v5.0.0 + uses: actions/setup-java@v5.2.0 with: distribution: 'temurin' java-version: ${{matrix.jdk}} @@ -58,7 +60,7 @@ jobs: - name: checkout code uses: actions/checkout@v5.0.0 - name: setup java - uses: actions/setup-java@v5.0.0 + uses: actions/setup-java@v5.2.0 with: distribution: 'temurin' java-version: 11 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index db079c1fa8..557fb3cc8c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,7 +18,7 @@ jobs: with: languages: 'java' - name: setup java - uses: actions/setup-java@v5.0.0 + uses: actions/setup-java@v5.2.0 with: java-version: '11' java-package: jdk diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7523245fa6..872ae1e0d4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -27,7 +27,7 @@ jobs: contents: read # This is required for actions/checkout steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ inputs.region }} role-to-assume: ${{ inputs.iam_role }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bea1c22d94..b8c00d690b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: with: ref: ${{inputs.branch}} - name: setup java - uses: actions/setup-java@v5.0.0 + uses: actions/setup-java@v5.2.0 with: distribution: 'temurin' java-version: '11' @@ -83,15 +83,15 @@ jobs: - name: Create GitHub Release id: create_release # Allow testing without creating a release - if: github.event_name != 'pull_request' && (github.event.ref == 'refs/heads/test' || startsWith(github.event.ref, 'refs/tags')) - uses: softprops/action-gh-release@v2.3.2 + if: github.event_name != 'pull_request' && (github.event.ref == 'refs/heads/develop' || startsWith(github.event.ref, 'refs/tags')) + uses: softprops/action-gh-release@v2.6.1 with: files: cwms-data-api/build/libs/cwms-data-api-${{env.VERSION}}.war tag_name: ${{env.VERSION}} generate_release_notes: true token: ${{ secrets.token != null && secrets.token || secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.11.1 + uses: docker/setup-buildx-action@v4.0.0 - name: Docker meta id: meta uses: docker/metadata-action@v5.8.0 @@ -110,13 +110,13 @@ jobs: type=schedule,pattern=${{inputs.branch}}-{{date 'YYYY.MM.DD-hhmmss'}} - name: Log in to the Container registry id: login-ghcr - uses: docker/login-action@v3.5.0 + uses: docker/login-action@v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.token != null && secrets.token || secrets.GITHUB_TOKEN }} - name: Login to HEC Public Registry - uses: docker/login-action@v3.5.0 + uses: docker/login-action@v4.0.0 id: login-hec with: registry: ${{ secrets.registry != null && secrets.registry ||secrets.HEC_PUB_REGISTRY }} diff --git a/Dockerfile b/Dockerfile index 4762bb8bc9..1b4d0a8a26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -56,6 +56,9 @@ ENV cwms.dataapi.access.providers="KeyAccessManager,OpenID" ENV cwms.dataapi.access.openid.wellKnownUrl="https:///.well-known/openid-configuration" ENV cwms.dataapi.access.openid.issuer="" ENV cwms.dataapi.access.openid.timeout="604800" +# Putting default values here to easy configuration +ENV cwms.dataapi.access.openid.clientId=cwms +ENV cwms.dataapi.access.openid.idpHint=federation-eams #ENV cwms.dataapi.access.openid.altAuthUrl="https://identityc-test.cwbi.us/auth/realms/cwbi" # used to simplify redeploy in certain contexts. Update to match - in image label diff --git a/cda-gui/.env.development b/cda-gui/.env.development index 3b6ad9684e..c86560b277 100644 --- a/cda-gui/.env.development +++ b/cda-gui/.env.development @@ -1 +1 @@ -CDA_API_ROOT=https://water.dev.cwbi.us/cwms-data \ No newline at end of file +VITE_CDA_API_ROOT=https://water.dev.cwbi.us/cwms-data \ No newline at end of file diff --git a/cda-gui/.env.production b/cda-gui/.env.production index 514b32727a..ef57f68558 100644 --- a/cda-gui/.env.production +++ b/cda-gui/.env.production @@ -1 +1 @@ -CDA_API_ROOT=https://cwms-data.usace.army.mil/cwms-data \ No newline at end of file +VITE_CDA_API_ROOT=https://cwms-data.usace.army.mil/cwms-data \ No newline at end of file diff --git a/cda-gui/.env.test b/cda-gui/.env.test index 85bd0b1a7b..6f12fa2e4d 100644 --- a/cda-gui/.env.test +++ b/cda-gui/.env.test @@ -1 +1 @@ -CDA_API_ROOT=https://cwms-data-test.cwbi.us/cwms-data \ No newline at end of file +VITE_CDA_API_ROOT=https://cwms-data-test.cwbi.us/cwms-data \ No newline at end of file diff --git a/cda-gui/package-lock.json b/cda-gui/package-lock.json index 439d02842e..6c196c45a7 100644 --- a/cda-gui/package-lock.json +++ b/cda-gui/package-lock.json @@ -17,7 +17,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-router-dom": "^7.1.2", - "swagger-ui-dist": "^5.17.7", + "swagger-ui-dist": "^5.29.5", "use-debounce": "^10.0.5" }, "devDependencies": { @@ -1033,18 +1033,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1493,6 +1481,13 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -2082,14 +2077,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -2269,6 +2256,24 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -3138,7 +3143,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/for-each": { "version": "0.3.3", @@ -4803,6 +4809,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5619,17 +5626,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5639,18 +5635,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -5870,9 +5854,13 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.7.tgz", - "integrity": "sha512-hKnq2Dss6Nvqxzj+tToBz0IJvKXgp7FExxX0Zj0rMajXJp8CJ98yLAwbKwKu8rxQf+2iIDUTGir84SCA8AN+fQ==" + "version": "5.32.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.1.tgz", + "integrity": "sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } }, "node_modules/tabbable": { "version": "6.2.0", @@ -5932,35 +5920,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/cda-gui/package.json b/cda-gui/package.json index 3f5439635a..950620627c 100644 --- a/cda-gui/package.json +++ b/cda-gui/package.json @@ -29,7 +29,7 @@ "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-router-dom": "^7.1.2", - "swagger-ui-dist": "^5.17.7", + "swagger-ui-dist": "^5.29.5", "use-debounce": "^10.0.5" }, "devDependencies": { diff --git a/cda-gui/public/oauth2-redirect.html b/cda-gui/public/oauth2-redirect.html new file mode 100644 index 0000000000..8d148cd103 --- /dev/null +++ b/cda-gui/public/oauth2-redirect.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/cda-gui/public/oauth2-redirect.js b/cda-gui/public/oauth2-redirect.js new file mode 100644 index 0000000000..e184625807 --- /dev/null +++ b/cda-gui/public/oauth2-redirect.js @@ -0,0 +1,85 @@ +"use strict"; +function run() { + var oauth2 = window.opener.swaggerUIRedirectOauth2; + var sentState = oauth2.state; + var redirectUrl = oauth2.redirectUrl; + var isValid, qp, arr; + + if (/code|token|error/.test(window.location.hash)) { + qp = window.location.hash.substring(1).replace("?", "&"); + } else { + qp = location.search.substring(1); + } + + arr = qp.split("&"); + arr.forEach(function (v, i, _arr) { + _arr[i] = '"' + v.replace("=", '":"') + '"'; + }); + qp = qp + ? JSON.parse("{" + arr.join() + "}", function (key, value) { + return key === "" ? value : decodeURIComponent(value); + }) + : {}; + + isValid = qp.state === sentState; + + if ( + (oauth2.auth.schema.get("flow") === "accessCode" || + oauth2.auth.schema.get("flow") === "authorizationCode" || + oauth2.auth.schema.get("flow") === "authorization_code") && + !oauth2.auth.code + ) { + if (!isValid) { + oauth2.errCb({ + authId: oauth2.auth.name, + source: "auth", + level: "warning", + message: + "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server", + }); + } + + if (qp.code) { + delete oauth2.state; + oauth2.auth.code = qp.code; + oauth2.callback({ auth: oauth2.auth, redirectUrl: redirectUrl }); + } else { + let oauthErrorMsg; + if (qp.error) { + oauthErrorMsg = + "[" + + qp.error + + "]: " + + (qp.error_description + ? qp.error_description + ". " + : "no accessCode received from the server. ") + + (qp.error_uri ? "More info: " + qp.error_uri : ""); + } + + oauth2.errCb({ + authId: oauth2.auth.name, + source: "auth", + level: "error", + message: + oauthErrorMsg || + "[Authorization failed]: no accessCode received from the server", + }); + } + } else { + oauth2.callback({ + auth: oauth2.auth, + token: qp, + isValid: isValid, + redirectUrl: redirectUrl, + }); + } + window.close(); +} + +if (document.readyState !== "loading") { + run(); +} else { + document.addEventListener("DOMContentLoaded", function () { + run(); + }); +} diff --git a/cda-gui/src/links/header-links.js b/cda-gui/src/links/header-links.js index c0ea959bd0..ec672bd372 100644 --- a/cda-gui/src/links/header-links.js +++ b/cda-gui/src/links/header-links.js @@ -17,7 +17,7 @@ export default [ { id: "swagger-schema", text: "Swagger Docs Schema", - href: "swagger-docs", + href: "/swagger-docs", }, ], }, diff --git a/cda-gui/src/pages/data-query/components/DataTabs.jsx b/cda-gui/src/pages/data-query/components/DataTabs.jsx index 6dc1ca8ad6..d9d76ac759 100644 --- a/cda-gui/src/pages/data-query/components/DataTabs.jsx +++ b/cda-gui/src/pages/data-query/components/DataTabs.jsx @@ -13,6 +13,7 @@ export default function DataTabs({ timeseriesParams, begin, end, + sortAscending, }) { if (!tsids || !tsids.length) return null; if (isLoading) return ; @@ -38,7 +39,7 @@ export default function DataTabs({ dateFormat="YYYY-MM-DD HH:mm:ss" interval="5" missingString="---" - sortAscending + sortAscending={sortAscending} trim pageSize={1000000} tableOptions={{ @@ -116,4 +117,5 @@ DataTabs.propTypes = { timeseriesParams: PropTypes.array, begin: PropTypes.object.isRequired, end: PropTypes.object.isRequired, + sortAscending: PropTypes.bool.isRequired, }; diff --git a/cda-gui/src/pages/data-query/components/SettingsGearButton.jsx b/cda-gui/src/pages/data-query/components/SettingsGearButton.jsx new file mode 100644 index 0000000000..17d74245e0 --- /dev/null +++ b/cda-gui/src/pages/data-query/components/SettingsGearButton.jsx @@ -0,0 +1,32 @@ +import { forwardRef } from "react"; +import PropTypes from "prop-types"; +import { FiSettings } from "react-icons/fi"; + +const SettingsGearButton = forwardRef(function SettingsGearButton( + { active = false, className = "", ...props }, + ref, +) { + const activeClassName = active + ? "border-red-200 bg-red-50 text-red-600 hover:bg-red-100" + : "border-slate-300 bg-white text-slate-700 hover:bg-slate-50"; + + return ( + + ); +}); + +SettingsGearButton.propTypes = { + active: PropTypes.bool, + className: PropTypes.string, +}; + +export default SettingsGearButton; diff --git a/cda-gui/src/pages/data-query/components/SettingsMenu.jsx b/cda-gui/src/pages/data-query/components/SettingsMenu.jsx new file mode 100644 index 0000000000..3de9072e9c --- /dev/null +++ b/cda-gui/src/pages/data-query/components/SettingsMenu.jsx @@ -0,0 +1,60 @@ +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; +import PropTypes from "prop-types"; +import Toggle from "./Toggle"; +import SettingsGearButton from "./SettingsGearButton"; + +export default function SettingsMenu({ + cacheEnabled, + setCacheEnabled, + sortAscending, + setSortAscending, + active, +}) { + return ( + + + + +
+
+
Enable cache
+

+ Use browser cache for repeated requests. Disable to force fresh fetches. +

+
+ +
+
+ +
+
+
+ Descending table order +
+

+ Keep newest timestamps first. Disable to switch to oldest rows first. +

+
+ setSortAscending(!checked)} + className="ml-0" + /> +
+
+
+
+ ); +} + +SettingsMenu.propTypes = { + active: PropTypes.bool, + cacheEnabled: PropTypes.bool.isRequired, + setCacheEnabled: PropTypes.func.isRequired, + setSortAscending: PropTypes.func.isRequired, + sortAscending: PropTypes.bool.isRequired, +}; diff --git a/cda-gui/src/pages/data-query/components/TimeSeriesDropdown.jsx b/cda-gui/src/pages/data-query/components/TimeSeriesDropdown.jsx index e3aa622ff8..294903b4af 100644 --- a/cda-gui/src/pages/data-query/components/TimeSeriesDropdown.jsx +++ b/cda-gui/src/pages/data-query/components/TimeSeriesDropdown.jsx @@ -14,7 +14,7 @@ import { useDebounce } from "use-debounce"; // Catalog client const catalogApi = new CatalogApi( new Configuration({ - basePath: import.meta.env.CDA_URL, + basePath: import.meta.env.VITE_CDA_API_ROOT, headers: { accept: "application/json;version=2" }, }), ); @@ -76,6 +76,11 @@ export default function TimeSeriesDropdown({ office, tsids, setTsids }) { onChange={(event) => setSearchTerm(event.target.value)} className="px-3 py-2 border rounded w-full" placeholder="Search TSID (e.g. Location.Elev.Inst.1Hour.0.Version)" + autoComplete="off" + autoCorrect="off" + autoCapitalize="none" + spellCheck={false} + name="tsid-search" /> {loading ? ( diff --git a/cda-gui/src/pages/data-query/hooks/useAliases.js b/cda-gui/src/pages/data-query/hooks/useAliases.js index 311e80a808..3e24bb784a 100644 --- a/cda-gui/src/pages/data-query/hooks/useAliases.js +++ b/cda-gui/src/pages/data-query/hooks/useAliases.js @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { CatalogApi, Configuration } from "cwmsjs"; -const CDA_URL = import.meta.env.CDA_URL; +const CDA_URL = import.meta.env.VITE_CDA_API_ROOT; const config = new Configuration({ basePath: CDA_URL, }); diff --git a/cda-gui/src/pages/data-query/hooks/useConfigList.js b/cda-gui/src/pages/data-query/hooks/useConfigList.js index 11b44f1864..6b091f5e62 100644 --- a/cda-gui/src/pages/data-query/hooks/useConfigList.js +++ b/cda-gui/src/pages/data-query/hooks/useConfigList.js @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { BlobApi, Configuration } from "cwmsjs"; -const CDA_URL = import.meta.env.CDA_URL; +const CDA_URL = import.meta.env.VITE_CDA_API_ROOT; const config = new Configuration({ basePath: CDA_URL, }); diff --git a/cda-gui/src/pages/data-query/hooks/useDescriptors.js b/cda-gui/src/pages/data-query/hooks/useDescriptors.js index 09016302e0..f472452bd6 100644 --- a/cda-gui/src/pages/data-query/hooks/useDescriptors.js +++ b/cda-gui/src/pages/data-query/hooks/useDescriptors.js @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { Configuration, CatalogApi } from "cwmsjs"; const config = new Configuration({ - basePath: import.meta.env.CDA_URL, + basePath: import.meta.env.VITE_CDA_API_ROOT, }); const cataApi = new CatalogApi(config); diff --git a/cda-gui/src/pages/data-query/hooks/useOffices.js b/cda-gui/src/pages/data-query/hooks/useOffices.js index a51b644d65..7300a30248 100644 --- a/cda-gui/src/pages/data-query/hooks/useOffices.js +++ b/cda-gui/src/pages/data-query/hooks/useOffices.js @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { Configuration, OfficesApi } from "cwmsjs"; -const CDA_URL = import.meta.env.CDA_URL; +const CDA_URL = import.meta.env.VITE_CDA_API_ROOT; const config = new Configuration({ basePath: CDA_URL, }); diff --git a/cda-gui/src/pages/data-query/hooks/useParams.js b/cda-gui/src/pages/data-query/hooks/useParams.js index 971b9e70cf..5b41d1e539 100644 --- a/cda-gui/src/pages/data-query/hooks/useParams.js +++ b/cda-gui/src/pages/data-query/hooks/useParams.js @@ -1,7 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import { Configuration, ParametersApi } from "cwmsjs"; -const CDA_URL = import.meta.env.CDA_URL; +const CDA_URL = import.meta.env.VITE_CDA_API_ROOT; const config = new Configuration({ basePath: CDA_URL, }); diff --git a/cda-gui/src/pages/data-query/index.jsx b/cda-gui/src/pages/data-query/index.jsx index a033440bea..ba8a98d8f5 100644 --- a/cda-gui/src/pages/data-query/index.jsx +++ b/cda-gui/src/pages/data-query/index.jsx @@ -12,6 +12,7 @@ import DataTabs from "./components/DataTabs"; import Toggle from "./components/Toggle"; import TimeSeriesBuilder from "./components/TimeSeriesBuilder"; import TimeSeriesManager from "./components/TimeSeriesManager"; +import SettingsMenu from "./components/SettingsMenu"; const CDA_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ssZ"; const v2_config = new Configuration({ @@ -21,6 +22,10 @@ const v2_config = new Configuration({ }); const ts_api = new TimeSeriesApi(v2_config); const offices_api = new OfficesApi(); +const DATA_QUERY_CACHE_KEY = "data-query-cache-enabled"; +const DATA_QUERY_SORT_ASC_KEY = "data-query-sort-ascending"; +const DEFAULT_CACHE_ENABLED = true; +const DEFAULT_SORT_ASCENDING = false; // const config = cwmsConfigs["SWF"]; // async function fetchConfig(configUrl) { @@ -32,6 +37,17 @@ const offices_api = new OfficesApi(); export default function DataQuery() { const [tsids, setTsids] = useState([]); const [visibleTSIDs, setVisibleTSIDs] = useState(tsids); + const [isRefreshing, setIsRefreshing] = useState(false); + const [cacheEnabled, setCacheEnabled] = useState(() => { + if (typeof window === "undefined") return DEFAULT_CACHE_ENABLED; + const storedValue = window.localStorage.getItem(DATA_QUERY_CACHE_KEY); + return storedValue === null ? DEFAULT_CACHE_ENABLED : storedValue === "true"; + }); + const [sortAscending, setSortAscending] = useState(() => { + if (typeof window === "undefined") return DEFAULT_SORT_ASCENDING; + const storedValue = window.localStorage.getItem(DATA_QUERY_SORT_ASC_KEY); + return storedValue === null ? DEFAULT_SORT_ASCENDING : storedValue === "true"; + }); // const [location, setLocation] = useState(null); // const [parameter, setParameter] = useState(null); // const [interval, setInterval] = useState(null); @@ -41,6 +57,16 @@ export default function DataQuery() { // Reset visible list when tsids change setVisibleTSIDs(tsids); }, [tsids]); + useEffect(() => { + if (typeof window !== "undefined") { + window.localStorage.setItem(DATA_QUERY_CACHE_KEY, String(cacheEnabled)); + } + }, [cacheEnabled]); + useEffect(() => { + if (typeof window !== "undefined") { + window.localStorage.setItem(DATA_QUERY_SORT_ASC_KEY, String(sortAscending)); + } + }, [sortAscending]); const toggleTSID = (tsid) => setVisibleTSIDs((prev) => @@ -61,7 +87,7 @@ export default function DataQuery() { const [beginDateTime, setBeginDateTime] = useState(dayjs().subtract(1, "day")); const [endDateTime, setEndDateTime] = useState(dayjs()); - async function fetchAllTSData(data) { + async function fetchAllTSData(data, requestOverrides) { let startDate = data?.begin; let endDate = data?.end; let values = data?.values; @@ -69,16 +95,19 @@ export default function DataQuery() { const maxPages = 200; let pageCount = 0; while (nextPage) { - let _result = await ts_api.getTimeSeries({ - begin: startDate, - end: endDate, - name, - office, - page: nextPage, - pageSize: 25000, - // begin: beginDateTime.format(CDA_DATE_FORMAT), - // end: endDateTime.format(CDA_DATE_FORMAT), - }); + let _result = await ts_api.getTimeSeries( + { + begin: startDate, + end: endDate, + name, + office, + page: nextPage, + pageSize: 25000, + // begin: beginDateTime.format(CDA_DATE_FORMAT), + // end: endDateTime.format(CDA_DATE_FORMAT), + }, + requestOverrides, + ); // if (!_result?.page) page = false nextPage = _result?.nextPage; endDate = _result?.end; @@ -102,24 +131,40 @@ export default function DataQuery() { const { data: timeseriesData, isLoading: timeseriesLoading, + refetch: refetchTimeseries, error, } = useQuery({ - queryKey: ["cdaTimeSeries", tsids, office, beginDateTime, endDateTime], + queryKey: [ + "cdaTimeSeries", + tsids, + office, + beginDateTime, + endDateTime, + cacheEnabled, + ], queryFn: async () => { + const requestOverrides = cacheEnabled + ? undefined + : { + cache: "no-store", + }; const promises = tsids.map((tsid) => { return ts_api - .getTimeSeriesRaw({ - name: tsid, - office: office, - begin: beginDateTime.format(CDA_DATE_FORMAT), - end: endDateTime.format(CDA_DATE_FORMAT), - pageSize: 25000, - }) + .getTimeSeriesRaw( + { + name: tsid, + office: office, + begin: beginDateTime.format(CDA_DATE_FORMAT), + end: endDateTime.format(CDA_DATE_FORMAT), + pageSize: 25000, + }, + requestOverrides, + ) .then(async (r) => { if (r.raw.ok) { let _data = await r.raw.json(); - return await fetchAllTSData(_data); + return await fetchAllTSData(_data, requestOverrides); } else return { name: tsid, values: [], message: r.raw.text }; }) .catch((e) => { @@ -141,6 +186,8 @@ export default function DataQuery() { tsid.split(".").every((part) => part.trim() !== ""), ) && office !== undefined, + staleTime: cacheEnabled ? 1000 * 60 * 5 : 0, + gcTime: cacheEnabled ? 1000 * 60 * 30 : 0, }); const timeseriesParams = useMemo(() => { @@ -224,6 +271,16 @@ export default function DataQuery() { link.click(); document.body.removeChild(link); }; + const handleRefreshTimeseries = async () => { + setIsRefreshing(true); + try { + await refetchTimeseries(); + } finally { + setIsRefreshing(false); + } + }; + const hasActiveSettings = + cacheEnabled !== DEFAULT_CACHE_ENABLED || sortAscending !== DEFAULT_SORT_ASCENDING; if (error) return ( @@ -239,6 +296,15 @@ export default function DataQuery() { return (
+
+ +
@@ -335,6 +401,15 @@ export default function DataQuery() { > Download JSON +
{timeseriesLoading ? ( @@ -360,6 +435,7 @@ export default function DataQuery() { isLoading={timeseriesLoading} cdaParams={cdaParams} timeseriesParams={timeseriesParams} + sortAscending={sortAscending} /> )}
diff --git a/cda-gui/src/pages/swagger-ui/index.jsx b/cda-gui/src/pages/swagger-ui/index.jsx index 62495d4b49..b7c411aabb 100644 --- a/cda-gui/src/pages/swagger-ui/index.jsx +++ b/cda-gui/src/pages/swagger-ui/index.jsx @@ -12,23 +12,53 @@ export default function SwaggerUI() { document.title = "CWMS Data API for Data Retrieval - Swagger UI"; // Begin Swagger UI call region // TODO: add endpoint that dynamic returns swagger generated doc - SwaggerUIBundle({ + + const ui = SwaggerUIBundle({ url: getBasePath() + "/swagger-docs", + dom_id: "#swagger-ui", deepLinking: false, presets: [SwaggerUIBundle.presets.apis], plugins: [SwaggerUIBundle.plugins.DownloadUrl], requestInterceptor: (req) => { - // Add a cache-busting query param - const sep = req.url.includes("?") ? "&" : "?"; - req.url = `${req.url}${sep}_cb=${Date.now()}`; - - // Also ask intermediaries not to serve from cache - req.headers["Cache-Control"] = "no-cache, no-store, max-age=0"; - req.headers["Pragma"] = "no-cache"; + // Add a cache-busting query param... but only if it's to our api. Some + // external systems, like keycloak, don't allow random unknown parameters. + const origin = window.location.origin; + const re = new RegExp(`^${origin}.*`); + if (re.test(req.url)) { + const sep = req.url.includes("?") ? "&" : "?"; + req.url = `${req.url}${sep}_cb=${Date.now()}`; + // Also ask intermediaries not to serve from cache + req.headers["Cache-Control"] = "no-cache, no-store, max-age=0"; + req.headers["Pragma"] = "no-cache"; + } return req; }, + onComplete: () => { + const spec = JSON.parse(ui.spec().get("spec")); + for (const schemeName in spec.components.securitySchemes) { + const scheme = spec.components.securitySchemes[schemeName]; + if (scheme.type === "openIdConnect") { + let additionalParams = null; + let hints = scheme["x-kc_idp_hint"]; + if (hints) { + additionalParams = { + // Since getting the interface to allow users to choose + // is likely impossible, we will assume the first in the list + // is the "primary" auth system + kc_idp_hint: hints.values[0], + }; + } + ui.initOAuth({ + clientId: scheme["x-oidc-client-id"], + usePkceWithAuthorizationCodeGrant: true, + additionalQueryStringParams: additionalParams, + }); + break; + } + } + }, }); }, []); diff --git a/cda-gui/vite.config.js b/cda-gui/vite.config.js index 5aef28e494..da3503371c 100644 --- a/cda-gui/vite.config.js +++ b/cda-gui/vite.config.js @@ -3,23 +3,30 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { - // const env = loadEnv(mode, process.cwd(), ""); + const env = loadEnv(mode, process.cwd(), ""); // const BASE_PATH = env?.BASE_PATH ?? "/cwms-data"; return { base: "/cwms-data", plugins: [react()], - define: { - "import.meta.env.CDA_URL": JSON.stringify("/cwms-data"), - }, server: { proxy: { "^/cwms-data/timeseries/.*": { - target: "https://cwms-data.usace.army.mil", + target: env.CDA_API_ROOT, changeOrigin: true, secure: false, }, "^/cwms-data/catalog/.*": { - target: "https://cwms-data.usace.army.mil", + target: env.CDA_API_ROOT, + changeOrigin: true, + secure: false, + }, + "^/cwms-data/auth/.*": { + target: env.CDA_API_ROOT, + changeOrigin: true, + secure: false, + }, + "^/cwms-data/swagger-docs$": { + target: env.CDA_API_ROOT, changeOrigin: true, secure: false, }, diff --git a/compose_files/keycloak/healthcheck.sh b/compose_files/keycloak/healthcheck.sh new file mode 100755 index 0000000000..33e0a39801 --- /dev/null +++ b/compose_files/keycloak/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +{ printf 'HEAD /auth/health/ready HTTP/1.0\r\n\r\n' >&0; grep 'HTTP/1.0 200'; } 0<>/dev/tcp/localhost/9000 diff --git a/compose_files/keycloak/realm.json b/compose_files/keycloak/realm.json index eddd4b5efb..2f9ff5ae4a 100644 --- a/compose_files/keycloak/realm.json +++ b/compose_files/keycloak/realm.json @@ -663,7 +663,8 @@ "clientAuthenticatorType": "client-secret", "redirectUris": [ "https://cwms-data.test:8444/*", - "https://localhost:5010/*" + "https://localhost:5010/*", + "http://localhost:*" ], "webOrigins": [ "*" diff --git a/compose_files/sql/users.sql b/compose_files/sql/users.sql index e882a8fb90..e8bacf1e37 100644 --- a/compose_files/sql/users.sql +++ b/compose_files/sql/users.sql @@ -1,22 +1,52 @@ -set define on + set define on define OFFICE_EROC=&1 +defin API_KEY=&2 begin - cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest','All Users', 'HQ'); - cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest','All Users', 'SPK'); - cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest','CWMS Users', 'HQ'); - cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest','CWMS User Admins', 'HQ'); - - - cwms_sec.add_cwms_user('l2hectest',NULL,'SPK'); - cwms_sec.update_edipi('l2hectest',1234567890); - cwms_sec.add_user_to_group('l2hectest','All Users', 'SPK'); - cwms_sec.add_user_to_group('l2hectest','CWMS Users', 'SPK'); - cwms_sec.add_user_to_group('l2hectest','TS ID Creator','SPK'); - - cwms_sec.add_cwms_user('l1hectest',NULL,'SPL'); + cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest', 'All Users', 'HQ'); + cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest', 'All Users', 'SPK'); + cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest', 'CWMS Users', 'HQ'); + cwms_sec.add_user_to_group('&&OFFICE_EROC.webtest', 'CWMS User Admins', 'HQ'); + cwms_sec.add_cwms_user('l2hectest', null, 'SPK'); + cwms_sec.update_edipi('l2hectest', 1234567890); + cwms_sec.add_user_to_group('l2hectest', 'All Users', 'SPK'); + cwms_sec.add_user_to_group('l2hectest', 'CWMS Users', 'SPK'); + cwms_sec.add_user_to_group('l2hectest', 'TS ID Creator', 'SPK'); + cwms_sec.add_cwms_user('l1hectest', null, 'SPL'); -- intentionally no extra permissions. --cwms_sec.add_user_to_group('l2hectest','CWMS Users', 'SPL'); + + cwms_sec.add_cwms_user('m5hectest', null, 'SWT'); + cwms_sec.add_user_to_group('m5hectest', 'All Users', 'SWT'); + cwms_sec.add_user_to_group('m5hectest', 'CWMS Users', 'SWT'); + cwms_sec.add_cwms_user('q0hectest', null, 'SWT'); + cwms_sec.add_user_to_group('q0hectest', 'All Users', 'SWT'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS Users', 'SWT'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS PD Users', 'SWT'); + cwms_sec.add_user_to_group('q0hectest', 'TS ID Creator', 'SWT'); + cwms_sec.add_cwms_user('q0hectest', null, 'MVP'); + cwms_sec.add_user_to_group('q0hectest', 'All Users', 'MVP'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS Users', 'MVP'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS PD Users', 'MVP'); + cwms_sec.add_user_to_group('q0hectest', 'TS ID Creator', 'MVP'); + cwms_sec.add_cwms_user('q0hectest', null, 'LRL'); + cwms_sec.add_user_to_group('q0hectest', 'All Users', 'LRL'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS Users', 'LRL'); + cwms_sec.add_user_to_group('q0hectest', 'CWMS PD Users', 'LRL'); + cwms_sec.add_user_to_group('q0hectest', 'TS ID Creator', 'LRL'); + execute immediate 'grant execute on cwms_20.cwms_upass to web_user'; + insert into "CWMS_20"."AT_API_KEYS" ( + userid, + key_name, + apikey, + created, + expires + ) values ( 'Q0HECTEST', + 'test', + '&&API_KEY', + to_date('2025-06-10 16:10:42','YYYY-MM-DD HH24:MI:SS'), + to_date('2029-06-16 16:10:46','YYYY-MM-DD HH24:MI:SS') ); + cwms_sec.add_cwms_user('m5hectest',NULL,'SWT'); cwms_sec.add_user_to_group('m5hectest','All Users', 'SWT'); cwms_sec.add_user_to_group('m5hectest','CWMS Users', 'SWT'); @@ -27,6 +57,7 @@ begin cwms_sec.add_user_to_group('m5testadmin','All Users', 'LRL'); cwms_sec.add_user_to_group('m5testadmin','CWMS Users', 'LRL'); cwms_sec.add_user_to_group('m5testadmin','CWMS User Admins', 'LRL'); + end; / quit; \ No newline at end of file diff --git a/cwms-data-api/build.gradle b/cwms-data-api/build.gradle index c2bc13bf19..36b0b03e26 100644 --- a/cwms-data-api/build.gradle +++ b/cwms-data-api/build.gradle @@ -151,6 +151,8 @@ dependencies { implementation(libs.bundles.overrides) testImplementation(libs.bundles.java.parser) + implementation(libs.togglz.core) + implementation(libs.minio) } task extractWebJars(type: Copy) { @@ -227,6 +229,7 @@ task run(type: JavaExec) { mainClass = "fixtures.TomcatServer" systemProperties += project.properties.findAll { k, v -> k.startsWith("RADAR") } systemProperties += project.properties.findAll { k, v -> k.startsWith("CDA") } + systemProperties += project.properties.findAll { k, v -> k.startsWith("cwms") } def context = project.findProperty("cda.war.context") ?: "spk-data" @@ -245,7 +248,7 @@ task run(type: JavaExec) { } task integrationTests(type: Test) { - dependsOn test +// dependsOn test dependsOn generateConfig dependsOn war diff --git a/cwms-data-api/logging.properties b/cwms-data-api/logging.properties index 5683fc81fe..1c937a444b 100644 --- a/cwms-data-api/logging.properties +++ b/cwms-data-api/logging.properties @@ -67,7 +67,8 @@ org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/cwms-data].handl fixtures.level=FINE cwms.cda.datasource.level=FINE -org.apache.level=FINE +org.apache.level=ERROR +org.apache.tomcat.jdbc.pool.level=INFO org.apache.catalina.realm.level=INFO org.apache.catalina.realm.useParentHandlers=true org.apache.catalina.authenticator.level=INFO diff --git a/cwms-data-api/src/docker/server.xml b/cwms-data-api/src/docker/server.xml index 408202f688..d5f2b3c7d9 100644 --- a/cwms-data-api/src/docker/server.xml +++ b/cwms-data-api/src/docker/server.xml @@ -47,6 +47,7 @@ connectionTimeout="20000" scheme="https" secure="true" acceptCount="500" disableUploadTimeout="true" server="cwms-data-api" maxThreads="200" relaxedPathChars="[]" relaxedQueryChars="[]" + maxPostSize="20971520" > diff --git a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java index d96ff6511d..02c20c99ff 100644 --- a/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java +++ b/cwms-data-api/src/main/java/cwms/cda/ApiServlet.java @@ -81,6 +81,7 @@ import cwms.cda.api.StreamController; import cwms.cda.api.StreamLocationController; import cwms.cda.api.StreamReachController; +import cwms.cda.api.VerticalDatumController; import cwms.cda.api.TextTimeSeriesController; import cwms.cda.api.TextTimeSeriesValueController; import cwms.cda.api.TimeSeriesCategoryController; @@ -212,6 +213,7 @@ import javax.sql.DataSource; import org.apache.http.entity.ContentType; import org.jetbrains.annotations.NotNull; +import org.jooq.exception.DataAccessException; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; @@ -380,6 +382,24 @@ public void init() { CdaError re = new CdaError(e.getMessage()); ctx.status(HttpServletResponse.SC_BAD_REQUEST).json(re); }) + .exception(DataAccessException.class, (e, ctx) -> { + // Whatever Dao is causing this exception to be thrown should be modified. + // The preferred pattern is for the Dao to catch DataAccessExceptions exceptions + // and for the dao to inspect the Oracle error code or error message as necessary + // to transform DataAccessExceptions (and their SQLException causes) + // into specific and appropriate exceptions with + // messages that are helpful and meaningful to end-users. + + // CdaError does not include the Oracle exception message b/c this block catches + // all unhandled DataAccessExceptions and we don't know what is in the message + // it is unknown if the message would be safe/appropriate for users to see. + CdaError errResponse = new CdaError("Database Error"); + logger.atWarning().withCause(e).log("error on request[%s]: %s", + errResponse.getIncidentIdentifier(), ctx.req.getRequestURI()); + ctx.status(500); + ctx.contentType(ContentType.APPLICATION_JSON.toString()); + ctx.json(errResponse); + }) .exception(Exception.class, (e, ctx) -> { CdaError errResponse = new CdaError("System Error"); logger.atWarning().withCause(e).log("error on request[%s]: %s", @@ -430,6 +450,14 @@ protected void configureRoutes() { get("/locations/with-kinds/", new LocationKindController(metrics)); cdaCrudCache("/locations/{location-id}", new LocationController(metrics), requiredRoles, 5, TimeUnit.MINUTES); + + VerticalDatumController vdiController = new VerticalDatumController(metrics); + String vdiPath = format("/location/{%s}/vertical-datum", Controllers.LOCATION_ID); + get(vdiPath, ctx -> vdiController.getOne(ctx, ctx.pathParam(Controllers.LOCATION_ID))); + addCacheControl(vdiPath, 5, TimeUnit.MINUTES); + post(vdiPath, vdiController::create, requiredRoles); + patch(vdiPath, ctx -> vdiController.update(ctx, ctx.pathParam(Controllers.LOCATION_ID)), requiredRoles); + delete(vdiPath, ctx -> vdiController.delete(ctx, ctx.pathParam(Controllers.LOCATION_ID)), requiredRoles); cdaCrudCache("/entity/{entity-id}", new EntityController(metrics), requiredRoles, 5, TimeUnit.MINUTES); cdaCrudCache("/states/{state}", diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BaseCrudHandler.java b/cwms-data-api/src/main/java/cwms/cda/api/BaseCrudHandler.java index 96d813cf32..d6b2e7f75b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BaseCrudHandler.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BaseCrudHandler.java @@ -23,12 +23,15 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; import io.javalin.apibuilder.CrudHandler; +import io.javalin.http.Context; import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.RESULTS; import static cwms.cda.api.Controllers.SIZE; public abstract class BaseCrudHandler implements CrudHandler { + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -46,4 +49,21 @@ protected final Timer.Context markAndTime(String subject) { protected final void updateResultSize(String responseString) { requestResultSize.update(responseString.length()); } + + protected final void updateResultSize(int responseLength) { + requestResultSize.update(responseLength); + } + + protected final void updateResultSize(long responseLength) { + requestResultSize.update(responseLength); + } + + public MetricRegistry getMetrics() { + return metrics; + } + + protected final void logUnusedPathParameter(Context ctx, String pathParam, String reason) { + String param = ctx.pathParam(pathParam); + LOGGER.atFinest().log("Path parameter '%s' is documented but not used in handler '%s'\nValue: '%s'\nReason: '%s'", pathParam, this.getClass().getSimpleName(), param, reason); + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BaseHandler.java b/cwms-data-api/src/main/java/cwms/cda/api/BaseHandler.java index 0a4ce454d0..db9dc01dbe 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BaseHandler.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BaseHandler.java @@ -23,6 +23,8 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; +import io.javalin.http.Context; import io.javalin.http.Handler; import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.RESULTS; @@ -30,6 +32,7 @@ public abstract class BaseHandler implements Handler { + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -47,4 +50,13 @@ protected final Timer.Context markAndTime(String subject) { protected final void updateResultSize(int value) { requestResultSize.update(value); } + + protected final void updateResultSize(long value) { + requestResultSize.update(value); + } + + protected final void logUnusedPathParameter(Context ctx, String pathParam, String reason) { + String param = ctx.pathParam(pathParam); + LOGGER.atFinest().log("Path parameter '%s' is documented but not used in handler '%s'\nValue: '%s'\nReason: '%s'", pathParam, this.getClass().getSimpleName(), param, reason); + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java b/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java index 65631c3589..c8bdf4f4a6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BasinController.java @@ -193,7 +193,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.EN.value()); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Basin.class); ctx.contentType(contentType.toString()); @@ -303,7 +303,7 @@ public void delete(@NotNull Context ctx, @NotNull String name) { cwms.cda.data.dao.basin.BasinDao basinDao = new cwms.cda.data.dao.basin.BasinDao(dsl); CwmsId basinId = new CwmsId.Builder() .withName(name) - .withOfficeId(ctx.queryParam(OFFICE)) + .withOfficeId(requiredParam(ctx, OFFICE)) .build(); basinDao.deleteBasin(basinId, deleteMethod.getRule()); StatusResponse re = new StatusResponse(basinId.getOfficeId(), "Deleted CWMS Basin", basinId.getName()); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesController.java index 767f1ed36f..8f6f1d0e1e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesController.java @@ -70,19 +70,18 @@ import org.jooq.DSLContext; -public class BinaryTimeSeriesController implements CrudHandler { +public class BinaryTimeSeriesController extends BaseCrudHandler { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); static final String TAG = "Binary-TimeSeries"; public static final String REPLACE_ALL = "replace-all"; private static final String DEFAULT_BIN_TYPE_MASK = "*"; public static final String BINARY_TYPE_MASK = "binary-type-mask"; - private final MetricRegistry metrics; public BinaryTimeSeriesController(MetricRegistry metrics) { - this.metrics = metrics; + super(metrics); } @NotNull @@ -91,11 +90,6 @@ protected TimeSeriesBinaryDao getDao(DSLContext dsl) { } - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); - } - - @OpenApi( summary = "Retrieve binary time series values for a provided time window and date version." + "If individual values exceed 64 kilobytes, a URL to a separate download is " @@ -179,7 +173,7 @@ public void getAll(@NotNull Context ctx) { @OpenApi(ignore = true) @Override public void getOne(@NotNull Context ctx, @NotNull String templateId) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -232,8 +226,8 @@ public void create(@NotNull Context ctx) { tags = {TAG} ) @Override - public void update(@NotNull Context ctx, @NotNull String oldBinaryTimeSeriesId) { - + public void update(@NotNull Context ctx, @NotNull String name) { + logUnusedPathParameter(ctx, NAME, "Body contains information"); try (Timer.Context ignored = markAndTime(UPDATE)) { boolean maxVersion = true; boolean replaceAll = ctx.queryParamAsClass(REPLACE_ALL, Boolean.class).getOrDefault(false); @@ -276,7 +270,7 @@ public void update(@NotNull Context ctx, @NotNull String oldBinaryTimeSeriesId) tags = {TAG} ) @Override - public void delete(@NotNull Context ctx, @NotNull String binaryTimeSeriesId) { + public void delete(@NotNull Context ctx, @NotNull String name) { try (Timer.Context ignored = markAndTime(DELETE)) { DSLContext dsl = getDslContext(ctx); String office = requiredParam(ctx, OFFICE); @@ -289,7 +283,7 @@ public void delete(@NotNull Context ctx, @NotNull String binaryTimeSeriesId) { TimeSeriesBinaryDao dao = getDao(dsl); - dao.delete(office, binaryTimeSeriesId, mask, begin, end, version); + dao.delete(office, name, mask, begin, end, version); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java index 76d668c014..d5e6b74a97 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BinaryTimeSeriesValueController.java @@ -24,39 +24,32 @@ package cwms.cda.api; -import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.BlobDao; +import cwms.cda.data.dao.StreamConsumer; +import io.javalin.core.util.Header; import io.javalin.http.Context; -import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import javax.servlet.http.HttpServletResponse; -import java.io.InputStream; -import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -public class BinaryTimeSeriesValueController implements Handler { - private final MetricRegistry metrics; - private final Histogram requestResultSize; - +public class BinaryTimeSeriesValueController extends BaseHandler { + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); public BinaryTimeSeriesValueController(MetricRegistry metrics) { - this.metrics = metrics; - requestResultSize = this.metrics.histogram((name(BinaryTimeSeriesValueController.class, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -67,14 +60,9 @@ private Timer.Context markAndTime(String subject) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + "the Binary TimeSeries whose data is to be included in the response."), - @OpenApiParam(name = TIMEZONE, description = "Specifies " - + "the time zone of the values of the begin and end fields (unless " - + "otherwise specified). If this field is not specified, " - + "the default time zone of UTC shall be used."), - @OpenApiParam(name = DATE, required = true, description = "The date of the binary value to retrieve"), - @OpenApiParam(name = VERSION_DATE, description = "The version date for the value to retrieve."), @OpenApiParam(name = BLOB_ID, description = "Will be removed in a schema update. " + - "This is a placeholder for integration testing with schema 23.3.16", deprecated = true) + "This is a placeholder for integration testing with schema 23.3.16", deprecated = true, + required = true) }, responses = { @OpenApiResponse(status = STATUS_200, @@ -84,26 +72,41 @@ private Timer.Context markAndTime(String subject) { )}, tags = {BinaryTimeSeriesController.TAG} ) - public void handle(Context ctx) { + public void handle(@NotNull Context ctx) { //Implementation will change with new CWMS schema //https://www.hec.usace.army.mil/confluence/display/CWMS/2024-02-29+Task2A+Text-ts+and+Binary-ts+Design + logUnusedPathParameter(ctx, NAME, "Handled as " + BLOB_ID + " in query parameter. May change with schema."); + try (Timer.Context ignored = markAndTime(GET_ALL)) { String binaryId = requiredParam(ctx, BLOB_ID); String officeId = requiredParam(ctx, OFFICE); DSLContext dsl = getDslContext(ctx); + + ctx.header(Header.ACCEPT_RANGES, "bytes"); + + final Long offset; + final Long end ; + long[] ranges = RangeParser.parseFirstRange(ctx.header(io.javalin.core.util.Header.RANGE)); + if (ranges != null) { + offset = ranges[0]; + end = ranges[1]; + } else { + offset = null; + end = null; + } + BlobDao blobDao = new BlobDao(dsl); - blobDao.getBlob(binaryId, officeId, (blob, mediaType) -> { - if (blob == null) { + StreamConsumer streamConsumer = (is, isPosition, mediaType, totalLength) -> { + if (is == null) { ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find " + "blob based on given parameters")); } else { - long size = blob.length(); - requestResultSize.update(size); - try (InputStream is = blob.getBinaryStream()) { - RangeRequestUtil.seekableStream(ctx, is, mediaType, size); - } + updateResultSize(totalLength); + RangeRequestUtil.seekableStream(ctx, is, isPosition, mediaType, totalLength); } - }); + }; + + blobDao.getBlob(binaryId, officeId, streamConsumer, offset, end); } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/BlobController.java b/cwms-data-api/src/main/java/cwms/cda/api/BlobController.java index f203713b58..0c796b127f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/BlobController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/BlobController.java @@ -8,8 +8,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.errors.CdaError; -import cwms.cda.data.dao.BlobDao; -import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.*; import cwms.cda.data.dto.Blob; import cwms.cda.data.dto.Blobs; import cwms.cda.data.dto.CwmsDTOPaginated; @@ -26,69 +25,81 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.io.InputStream; + import java.util.Optional; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; +import org.togglz.core.context.FeatureContext; +import cwms.cda.features.CdaFeatures; +import org.togglz.core.manager.FeatureManager; /** * */ -public class BlobController implements CrudHandler { +public class BlobController extends BaseCrudHandler { private static final int DEFAULT_PAGE_SIZE = 20; public static final String TAG = "Blob"; - private final MetricRegistry metrics; - - - private final Histogram requestResultSize; - public BlobController(MetricRegistry metrics) { - this.metrics = metrics; - String className = BlobController.class.getName(); - - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } protected DSLContext getDslContext(Context ctx) { return JooqDao.getDslContext(ctx); } + private BlobAccess chooseBlobAccess(DSLContext dsl) { + boolean useObjectStore = false; + try { + FeatureManager featureManager = FeatureContext.getFeatureManager(); + useObjectStore = featureManager.isActive(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + } catch (Throwable ignore) { + // fall back to system/env property check + } + if (useObjectStore) { + ObjectStorageConfig cfg = ObjectStorageConfig.fromSystem(); + return new ObjectStorageBlobDao(cfg); + } + return new BlobDao(dsl); + } + + @OpenApi( - queryParams = { - @OpenApiParam(name = OFFICE, - description = "Specifies the owning office. If this field is not " - + "specified, matching information from all offices shall be " - + "returned."), - @OpenApiParam(name = PAGE, - description = "This end point can return a lot of data, this " - + "identifies where in the request you are. This is an opaque" - + " value, and can be obtained from the 'next-page' value in " - + "the response."), - @OpenApiParam(name = PAGE_SIZE, - type = Integer.class, - description = "How many entries per page returned. Default " - + DEFAULT_PAGE_SIZE + "."), - @OpenApiParam(name = LIKE, - description = "Posix regular expression " - + "describing the blob id's you want") - }, - responses = {@OpenApiResponse(status = STATUS_200, - description = "A list of blobs.", - content = { - @OpenApiContent(type = Formats.JSON, from = Blobs.class), - @OpenApiContent(type = Formats.JSONV2, from = Blobs.class), - }) - }, - tags = {TAG} + queryParams = { + @OpenApiParam(name = OFFICE, + description = "Specifies the owning office. If this field is not " + + "specified, matching information from all offices shall be " + + "returned."), + @OpenApiParam(name = PAGE, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), + @OpenApiParam(name = PAGE_SIZE, + type = Integer.class, + description = "How many entries per page returned. Default " + + DEFAULT_PAGE_SIZE + "."), + @OpenApiParam(name = LIKE, + description = "Posix regular expression " + + "describing the blob id's you want") + }, + responses = {@OpenApiResponse(status = STATUS_200, + description = "A list of blobs.", + content = { + @OpenApiContent(type = Formats.JSON, from = Blobs.class), + @OpenApiContent(type = Formats.JSONV2, from = Blobs.class), + }) + }, + tags = {TAG} ) @Override public void getAll(@NotNull Context ctx) { @@ -98,7 +109,7 @@ public void getAll(@NotNull Context ctx) { String office = ctx.queryParam(OFFICE); String cursor = queryParamAsClass(ctx, new String[]{PAGE, CURSOR}, - String.class, "", metrics, name(BlobController.class.getName(), GET_ALL)); + String.class, "", getMetrics(), name(BlobController.class.getName(), GET_ALL)); if (!CwmsDTOPaginated.CURSOR_CHECK.invoke(cursor)) { ctx.json(new CdaError("cursor or page passed in but failed validation")) @@ -107,7 +118,7 @@ public void getAll(@NotNull Context ctx) { } int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE}, - Integer.class, DEFAULT_PAGE_SIZE, metrics, + Integer.class, DEFAULT_PAGE_SIZE, getMetrics(), name(BlobController.class.getName(), GET_ALL)); String like = ctx.queryParamAsClass(LIKE, String.class).getOrDefault(".*"); @@ -115,35 +126,43 @@ public void getAll(@NotNull Context ctx) { String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Blobs.class); - BlobDao dao = new BlobDao(dsl); + BlobAccess dao = chooseBlobAccess(dsl); Blobs blobs = dao.getBlobs(cursor, pageSize, office, like); String result = Formats.format(contentType, blobs); ctx.result(result); ctx.contentType(contentType.toString()); - requestResultSize.update(result.length()); + updateResultSize(result.length()); } } @OpenApi( description = "Returns the binary value of the requested blob as a seekable stream with the " + "appropriate media type.", + pathParams = { + @OpenApiParam(name = BLOB_ID, description = "If the _query_ parameter is provided this _path_ parameter " + + "is ignored and the value of the query parameter is used. " + + "Note: the _query_ parameter is necessary for id's that contain '/' or other special " + + "characters. This is due to limitations in path pattern matching. " + + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " + + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), + }, queryParams = { - @OpenApiParam(name = OFFICE, description = "Specifies the owning office."), - @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " - + "is ignored and the value of the query parameter is used. " - + "Note: this query parameter is necessary for id's that contain '/' or other special " - + "characters. This is due to limitations in path pattern matching. " - + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " - + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), + @OpenApiParam(name = OFFICE, description = "Specifies the owning office."), + @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " + + "is ignored and the value of the query parameter is used. " + + "Note: this query parameter is necessary for id's that contain '/' or other special " + + "characters. This is due to limitations in path pattern matching. " + + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " + + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), }, responses = { - @OpenApiResponse(status = STATUS_200, - description = "Returns requested blob.", - content = { - @OpenApiContent(type = "application/octet-stream") - }) + @OpenApiResponse(status = STATUS_200, + description = "Returns requested blob.", + content = { + @OpenApiContent(type = "application/octet-stream", from = byte[].class) + }) }, tags = {TAG} ) @@ -156,27 +175,40 @@ public void getOne(@NotNull Context ctx, @NotNull String blobId) { blobId = idQueryParam; } DSLContext dsl = getDslContext(ctx); - BlobDao dao = new BlobDao(dsl); + + BlobAccess dao = chooseBlobAccess(dsl); String officeQP = ctx.queryParam(OFFICE); Optional office = Optional.ofNullable(officeQP); - BlobDao.BlobConsumer tripleConsumer = (blob, mediaType) -> { - if (blob == null) { + final Long offset; + final Long end; + long[] ranges = RangeParser.parseFirstRange(ctx.header(io.javalin.core.util.Header.RANGE)); + if (ranges != null) { + offset = ranges[0]; + end = ranges[1]; + } else { + offset = null; + end = null; + } + + ctx.header(Header.ACCEPT_RANGES, "bytes"); + + StreamConsumer consumer = (is, isPosition, mediaType, totalLength) -> { + if (is == null) { ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find " + "blob based on given parameters")); } else { - long size = blob.length(); - requestResultSize.update(size); - try (InputStream is = blob.getBinaryStream()) { // is OracleBlobInputStream - RangeRequestUtil.seekableStream(ctx, is, mediaType, size); - } + updateResultSize(totalLength); + // is OracleBlobInputStream or something from MinIO + RangeRequestUtil.seekableStream(ctx, is, isPosition, mediaType, totalLength); } }; + if (office.isPresent()) { - dao.getBlob(blobId, office.get(), tripleConsumer); + dao.getBlob(blobId, office.get(), consumer, offset, end); } else { - dao.getBlob(blobId, tripleConsumer); + dao.getBlob(blobId, null, consumer, offset, end); } } } @@ -185,13 +217,13 @@ public void getOne(@NotNull Context ctx, @NotNull String blobId) { @OpenApi( description = "Create new Blob", requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = Blob.class, type = Formats.JSONV2) - }, - required = true), + content = { + @OpenApiContent(from = Blob.class, type = Formats.JSONV2) + }, + required = true), queryParams = { - @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, - description = "Create will fail if provided ID already exists. Default: true") + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided ID already exists. Default: true") }, method = HttpMethod.POST, tags = {TAG} @@ -204,7 +236,7 @@ public void create(@NotNull Context ctx) { boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); ContentType contentType = Formats.parseHeader(formatHeader, Blob.class); Blob blob = Formats.parseContent(contentType, ctx.bodyAsInputStream(), Blob.class); - BlobDao dao = new BlobDao(dsl); + BlobAccess dao = chooseBlobAccess(dsl); dao.create(blob, failIfExists, false); ctx.status(HttpCode.CREATED); } @@ -213,27 +245,29 @@ public void create(@NotNull Context ctx) { @OpenApi( description = "Update an existing Blob", pathParams = { - @OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"), + @OpenApiParam(name = BLOB_ID, description = "The blob identifier to be updated"), }, requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = Blob.class, type = Formats.JSONV2), - @OpenApiContent(from = Blob.class, type = Formats.JSON) - }, - required = true), + content = { + @OpenApiContent(from = Blob.class, type = Formats.JSONV2), + @OpenApiContent(from = Blob.class, type = Formats.JSON) + }, + required = true), queryParams = { - @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " - + "is ignored and the value of the query parameter is used. " - + "Note: this query parameter is necessary for id's that contain '/' or other special " - + "characters. This is due to limitations in path pattern matching. " - + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " - + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), + @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " + + "is ignored and the value of the query parameter is used. " + + "Note: this query parameter is necessary for id's that contain '/' or other special " + + "characters. This is due to limitations in path pattern matching. " + + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " + + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), }, method = HttpMethod.PATCH, tags = {TAG} ) @Override public void update(@NotNull Context ctx, @NotNull String blobId) { + logUnusedPathParameter(ctx, BLOB_ID, "Body contains information"); + try (final Timer.Context ignored = markAndTime(UPDATE)) { String idQueryParam = ctx.queryParam(BLOB_ID); if (idQueryParam != null) { @@ -260,7 +294,13 @@ public void update(@NotNull Context ctx, @NotNull String blobId) { + "updating a blob"); } - BlobDao dao = new BlobDao(dsl); + if (!blob.getId().equals(blobId)) { + throw new FormattingException("The blob id parameter does not match the blob id in the body. " + + "The blob end-point does not support renaming blobs. " + + "Create a new blob with the new id and delete the old one."); + } + + BlobAccess dao = chooseBlobAccess(dsl); dao.update(blob, false); ctx.status(HttpServletResponse.SC_OK); } @@ -269,17 +309,17 @@ public void update(@NotNull Context ctx, @NotNull String blobId) { @OpenApi( description = "Deletes requested blob", pathParams = { - @OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"), + @OpenApiParam(name = BLOB_ID, description = "The blob identifier to be deleted"), }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " - + "owning office of the blob to be deleted"), - @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " - + "is ignored and the value of the query parameter is used. " - + "Note: this query parameter is necessary for id's that contain '/' or other special " - + "characters. This is due to limitations in path pattern matching. " - + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " - + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + + "owning office of the blob to be deleted"), + @OpenApiParam(name = BLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " + + "is ignored and the value of the query parameter is used. " + + "Note: this query parameter is necessary for id's that contain '/' or other special " + + "characters. This is due to limitations in path pattern matching. " + + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " + + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), }, method = HttpMethod.DELETE, tags = {TAG} @@ -293,7 +333,7 @@ public void delete(@NotNull Context ctx, @NotNull String blobId) { } DSLContext dsl = getDslContext(ctx); String office = requiredParam(ctx, OFFICE); - BlobDao dao = new BlobDao(dsl); + BlobAccess dao = chooseBlobAccess(dsl); dao.delete(office, blobId); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java index f21784f0b2..a4d04a7e40 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/CatalogController.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.Set; import com.google.common.flogger.FluentLogger; +import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.owasp.html.PolicyFactory; @@ -63,19 +64,19 @@ private Timer.Context markAndTime(String subject) { @OpenApi(tags = {TAG}, ignore = true) @Override public void create(Context ctx) { - ctx.status(HttpCode.NOT_IMPLEMENTED).result("cannot perform this action"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(tags = {"Catalog"}, ignore = true) @Override public void delete(Context ctx, @NotNull String entry) { - ctx.status(HttpCode.NOT_IMPLEMENTED).result("cannot perform this action"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(tags = {"Catalog"}, ignore = true) @Override public void getAll(Context ctx) { - ctx.status(HttpCode.NOT_IMPLEMENTED).result("cannot perform this action"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -84,7 +85,11 @@ public void getAll(Context ctx) { description = "This end point can return a lot of data, this " + "identifies where in the request you are." ), - + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. Default 500." @@ -311,7 +316,7 @@ private static void warnAboutNotSupported(@NotNull Context ctx, String[] warnAbo @OpenApi(tags = {"Catalog"}, ignore = true) @Override public void update(Context ctx, @NotNull String entry) { - ctx.status(HttpCode.NOT_IMPLEMENTED).json(CdaError.notImplemented()); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ClobController.java b/cwms-data-api/src/main/java/cwms/cda/api/ClobController.java index 62e5ed6a22..f14a128ac7 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ClobController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ClobController.java @@ -27,6 +27,7 @@ import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.ClobDao; import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.StreamConsumer; import cwms.cda.data.dto.Clob; import cwms.cda.data.dto.Clobs; import cwms.cda.data.dto.CwmsDTOPaginated; @@ -43,7 +44,7 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.io.InputStream; + import java.util.Objects; import java.util.Optional; import javax.servlet.http.HttpServletResponse; @@ -85,6 +86,11 @@ protected DSLContext getDslContext(Context ctx) { + "identifies where in the request you are. This is an opaque" + " value, and can be obtained from the 'next-page' value in " + "the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. Default " @@ -151,6 +157,14 @@ public void getAll(@NotNull Context ctx) { + "When the accept header is set to " + Formats.JSONV2 + " the clob will be returned as a serialized Clob " + "object with fields for office-id, id, description and value. " + "For more information about accept header usage, see this page.", + pathParams = { + @OpenApiParam(name = CLOB_ID, description = "If the _query_ parameter is provided this _path_ parameter " + + "is ignored and the value of the query parameter is used. " + + "Note: the query parameter is necessary for id's that contain '/' or other special " + + "characters. This is due to limitations in path pattern matching. " + + "We will likely add support for encoding the ID in the path in the future. For now use the id field for those IDs. " + + "Client libraries should detect slashes and choose the appropriate field. \"ignored\" is suggested for the path endpoint."), + }, queryParams = { @OpenApiParam(name = OFFICE, description = "Specifies the owning office."), @OpenApiParam(name = CLOB_ID, description = "If this _query_ parameter is provided the id _path_ parameter " @@ -187,16 +201,16 @@ public void getOne(@NotNull Context ctx, @NotNull String clobId) { if (TEXT_PLAIN.equals(formatHeader)) { // useful cmd: curl -X 'GET' 'http://localhost:7000/cwms-data/clobs/encoded?office=SPK&id=%2FTIME%20SERIES%20TEXT%2F6261044' // -H 'accept: text/plain' --header "Range: bytes=20000-40000" - dao.getClob(clobId, office, c -> { - if (c == null) { - ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find " - + "clob based on given parameters")); - } else { - try (InputStream is = c.getAsciiStream()) { - RangeRequestUtil.seekableStream(ctx, is, TEXT_PLAIN, c.length()); - } - } - }); + + ctx.header(Header.ACCEPT_RANGES, "bytes"); + + StreamConsumer consumer = (is, isPosition, mediaType, totalLength) -> { + requestResultSize.update(totalLength); + RangeRequestUtil.seekableStream(ctx, is, isPosition, mediaType, totalLength); + }; + + dao.getClob(clobId, office, consumer); + } else { Optional optAc = dao.getByUniqueName(clobId, office); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java index 3528a22e76..e4059e4e16 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/Controllers.java @@ -147,6 +147,7 @@ public final class Controllers { public static final String LOCATION_ID = "location-id"; public static final String SOURCE_ENTITY = "source-entity"; + public static final String SOURCE_ENTITY_LIKE = "source-entity-like"; public static final String FORECAST_DATE = "forecast-date"; public static final String ISSUE_DATE = "issue-date"; public static final String LOCATION_KIND_LIKE = "location-kind-like"; @@ -222,6 +223,7 @@ public final class Controllers { public static final String PREFIX = "prefix"; public static final String PROJECT_LIKE = "project-like"; + public static final String USERNAME_LIKE = "username-like"; public static final String APPLICATION_ID = "application-id"; public static final String REVOKE_EXISTING = "revoke-existing"; public static final String REVOKE_TIMEOUT = "revoke-timeout"; @@ -241,6 +243,7 @@ public final class Controllers { private static final String DEPRECATED_CSV = "2024-11-01 CSV is not used often."; public static final String QUERY = "query"; + public static final String INCLUDE_ROLES = "include-roles"; static { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java b/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java index 1db14e6160..12da354312 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/EmbankmentController.java @@ -90,7 +90,7 @@ private Timer.Context markAndTime(String subject) { @Override public void getAll(Context ctx) { String office = ctx.queryParam(OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String projectId = requiredParam(ctx, PROJECT_ID); try (Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); EmbankmentDao dao = new EmbankmentDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ForecastFileController.java b/cwms-data-api/src/main/java/cwms/cda/api/ForecastFileController.java index 0fcd431c72..607d59492f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ForecastFileController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ForecastFileController.java @@ -29,16 +29,18 @@ import com.codahale.metrics.Timer; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.ForecastInstanceDao; +import cwms.cda.data.dao.StreamConsumer; import cwms.cda.helpers.DateUtils; +import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import org.jetbrains.annotations.NotNull; import javax.servlet.http.HttpServletResponse; -import java.io.InputStream; import java.time.Instant; import static com.codahale.metrics.MetricRegistry.name; @@ -92,8 +94,8 @@ private Timer.Context markAndTime(String subject) { }, tags = {ForecastSpecController.TAG} ) - public void handle(Context ctx) { - String specId = requiredParam(ctx, NAME); + public void handle(@NotNull Context ctx) { + String specId = ctx.pathParam(NAME); String office = requiredParam(ctx, OFFICE); String designator = ctx.queryParamAsClass(DESIGNATOR, String.class).allowNullable().get(); String forecastDate = requiredParam(ctx, FORECAST_DATE); @@ -102,18 +104,17 @@ public void handle(Context ctx) { Instant issueInstant = DateUtils.parseUserDate(issueDate, "UTC").toInstant(); try (Timer.Context ignored = markAndTime(GET_ALL)) { ForecastInstanceDao dao = new ForecastInstanceDao(getDslContext(ctx)); - dao.getFileBlob(office, specId, designator, forecastInstant, issueInstant, (blob, mediaType) -> { - if (blob == null) { + StreamConsumer streamConsumer = (is, isPosition, mediaType, totalLength) -> { + if (is == null) { ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find " + "blob based on given parameters")); } else { - long size = blob.length(); - requestResultSize.update(size); - try (InputStream is = blob.getBinaryStream()) { - RangeRequestUtil.seekableStream(ctx, is, mediaType, size); - } + requestResultSize.update(totalLength); + ctx.header(Header.ACCEPT_RANGES, "bytes"); + RangeRequestUtil.seekableStream(ctx, is, isPosition, mediaType, totalLength); } - }); + }; + dao.getFileBlob(office, specId, designator, forecastInstant, issueInstant, streamConsumer); } } } \ No newline at end of file diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ForecastInstanceController.java b/cwms-data-api/src/main/java/cwms/cda/api/ForecastInstanceController.java index 0899db67d3..e7ed5292e4 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ForecastInstanceController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ForecastInstanceController.java @@ -33,22 +33,13 @@ import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -public final class ForecastInstanceController implements CrudHandler { +public final class ForecastInstanceController extends BaseCrudHandler { public static final String TAG = "Forecast"; - private final MetricRegistry metrics; - - private final Histogram requestResultSize; private static final int KILO_BYTE_LIMIT = Integer.parseInt(System.getProperty("cda.api.forecast.file.max.length.kB", "64")); public ForecastInstanceController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } protected DSLContext getDslContext(Context ctx) { @@ -172,7 +163,7 @@ public void getAll(@NotNull Context ctx) { String result = Formats.format(contentType, instances, ForecastInstance.class); ctx.result(result).contentType(contentType.toString()); - requestResultSize.update(result.length()); + updateResultSize(result.length()); ctx.status(HttpServletResponse.SC_OK); } catch (URISyntaxException e) { @@ -242,7 +233,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { String result = Formats.format(contentType, instance); ctx.result(result).contentType(contentType.toString()); - requestResultSize.update(result.length()); + updateResultSize(result.length()); ctx.status(HttpServletResponse.SC_OK); } catch (URISyntaxException e) { @@ -271,6 +262,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { ) @Override public void update(@NotNull Context ctx, @NotNull String name) { + logUnusedPathParameter(ctx, NAME, "Body contains information"); try (final Timer.Context ignored = markAndTime(UPDATE)) { ForecastInstance forecastInstance = deserializeForecastInstance(ctx); ForecastInstanceDao dao = new ForecastInstanceDao(getDslContext(ctx)); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ForecastSpecController.java b/cwms-data-api/src/main/java/cwms/cda/api/ForecastSpecController.java index d991dab7b7..1036fc1922 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ForecastSpecController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ForecastSpecController.java @@ -1,9 +1,5 @@ package cwms.cda.api; -import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.*; - -import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.data.dao.DeleteRule; @@ -12,7 +8,6 @@ import cwms.cda.data.dto.forecast.ForecastSpec; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.plugin.openapi.annotations.HttpMethod; @@ -21,27 +16,18 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.io.IOException; import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; +import static cwms.cda.api.Controllers.*; -public final class ForecastSpecController implements CrudHandler { +public final class ForecastSpecController extends BaseCrudHandler { public static final String TAG = "Forecast"; - private final MetricRegistry metrics; - - private final Histogram requestResultSize; public ForecastSpecController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } protected DSLContext getDslContext(Context ctx) { @@ -140,7 +126,8 @@ public void delete(@NotNull Context ctx, @NotNull String name) { + "Default behavior when this parameter is not provided is to search for forecast " + "specifications with a null designator. "), @OpenApiParam(name = SOURCE_ENTITY, description = "Specifies the source identity " - + "of the forecast spec whose data is to be included in the response.") + + "of the forecast spec whose data is to be included in the response. Interpreted as a regular expression."), + @OpenApiParam(name = SOURCE_ENTITY_LIKE, description = "Specifies the source entity using LIKE-style matching. If provided, this parameter is used instead of the regular expression parameter 'source-entity'.") }, responses = { @OpenApiResponse(status = STATUS_200, @@ -161,19 +148,20 @@ public void getAll(@NotNull Context ctx) { String names = ctx.queryParamAsClass(ID_MASK, String.class).getOrDefault("*"); String designator = ctx.queryParamAsClass(DESIGNATOR_MASK, String.class).allowNullable().get(); String sourceEntity = ctx.queryParamAsClass(SOURCE_ENTITY, String.class).getOrDefault("*"); + String entityLike = ctx.queryParamAsClass(SOURCE_ENTITY_LIKE, String.class).allowNullable().get(); DSLContext dsl = getDslContext(ctx); ForecastSpecDao dao = new ForecastSpecDao(dsl); List specs = dao.getForecastSpecs(office, names, designator, - sourceEntity); + sourceEntity, entityLike); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, ForecastSpec.class); String result = Formats.format(contentType, specs, ForecastSpec.class); ctx.result(result).contentType(contentType.toString()); - requestResultSize.update(result.length()); + updateResultSize(result.length()); ctx.status(HttpServletResponse.SC_OK); } @@ -222,7 +210,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { String result = Formats.format(contentType, spec); ctx.result(result).contentType(contentType.toString()); - requestResultSize.update(result.length()); + updateResultSize(result.length()); ctx.status(HttpServletResponse.SC_OK); } @@ -247,6 +235,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { ) @Override public void update(@NotNull Context ctx, @NotNull String name) { + logUnusedPathParameter(ctx, NAME, "Body contains information"); try (final Timer.Context ignored = markAndTime(UPDATE)) { ForecastSpec forecastSpec = deserializeForecastSpec(ctx); DSLContext dsl = getDslContext(ctx); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ForecastTimeseriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/ForecastTimeseriesController.java index ce604c26c3..4aed7436b9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ForecastTimeseriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ForecastTimeseriesController.java @@ -3,6 +3,7 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.TimeSeries; import cwms.cda.formatters.Formats; @@ -13,6 +14,7 @@ import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; +import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -45,40 +47,10 @@ private Timer.Context markAndTime(String subject) { return Controllers.markAndTime(metrics, getClass().getName(), subject); } - @OpenApi( - description = "Used to create and save a forecast timeseries", - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = TimeSeries.class, type = Formats.JSONV2) - }, - required = true - ), - queryParams = { - @OpenApiParam(name = FORECAST_DATE, required = true, description = "Specifies the " + - "forecast date time of the forecast instance to be associated with the created" + - "forecast timeseries."), - @OpenApiParam(name = ISSUE_DATE, required = true, description = "Specifies the " + - "issue date time of the forecast instance to be associated with the created " + - "forecast timeseries."), - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + - "owning office of the forecast spec whose forecast instance will be " + - "associated with the created forecast timeseries."), - @OpenApiParam(name = NAME, required = true, description = "Specifies the " + - "spec id of the forecast spec whose forecast instance will be " + - "associated with the created forecast timeseries."), - @OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the " + - "location of the forecast spec whose forecast instance will be" + - "associated with the created forecast timeseries."), - @OpenApiParam(name = TIMESERIES_ID, required = true, description = "Id of timeseries " + - "that will be created.") - }, - method = HttpMethod.POST, - path = "/forecast-timeseries", - tags = TAG - ) + @OpenApi(ignore = true) @Override public void create(@NotNull Context ctx) { - + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } protected DSLContext getDslContext(Context ctx) { @@ -88,34 +60,25 @@ protected DSLContext getDslContext(Context ctx) { @OpenApi(ignore = true) @Override public void delete(@NotNull Context ctx, @NotNull String forecastSpecId) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void getAll(@NotNull Context ctx) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void getOne(@NotNull Context ctx, @NotNull String id) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void update(@NotNull Context ctx, @NotNull String id) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsAsTimeSeriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsAsTimeSeriesController.java index 9927c069c9..3b2537ae93 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsAsTimeSeriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsAsTimeSeriesController.java @@ -50,15 +50,10 @@ import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -public class LevelsAsTimeSeriesController implements Handler { - private final MetricRegistry metrics; +public class LevelsAsTimeSeriesController extends BaseHandler { public LevelsAsTimeSeriesController(MetricRegistry metrics) { - this.metrics = metrics; - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -113,7 +108,7 @@ private Timer.Context markAndTime(String subject) { tags = LevelsController.TAG ) public void handle(Context ctx) { - + logUnusedPathParameter(ctx, LEVEL_ID, "Body contains required information"); try (final Timer.Context timeContext = markAndTime("getLevelAsTimeSeries")) { DSLContext dsl = getDslContext(ctx); Validator pathParam = ctx.pathParamAsClass(LEVEL_ID, String.class); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java index c1dfa0de36..f6ec6f48b8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LevelsController.java @@ -55,6 +55,7 @@ import cwms.cda.formatters.FormattingException; import cwms.cda.formatters.UnsupportedFormatException; import cwms.cda.helpers.DateUtils; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -161,6 +162,9 @@ private LocationLevel deserializeLocationLevel(Context ctx) throws IOException { @OpenApiParam(name = EFFECTIVE_DATE, description = "Specifies the " + "effective date of the level to be deleted. If not provided will " + "delete all data and reference to the location level."), + @OpenApiParam(name = DATE, deprecated = true, description = "Specifies " + + "the effective date of Location Level that will be deleted." + + " Deprecated, use " + EFFECTIVE_DATE + " instead"), @OpenApiParam(name = TIMEZONE, description = "Specifies the time zone of " + "the value of the effective date field (unless otherwise " + "specified).If this field is not specified, the default time zone of UTC " @@ -196,6 +200,9 @@ public void delete(@NotNull Context ctx, @NotNull String levelId) { @OpenApiParam(name = LEVEL_ID_MASK, description = "Specifies the name(s) of " + "the location level(s) whose data is to be included in the response. " + "Uses * for all."), + @OpenApiParam(name = NAME, deprecated = true, description = "Specifies the name(s) of " + + "the location level(s) whose data is to be included in the response. " + + "Uses * for all. Deprecated, use " + LEVEL_ID_MASK + " instead"), @OpenApiParam(name = OFFICE, description = "Specifies the owning " + "office of the location level(s) whose data is to be included in the" + " response. If this field is not specified, matching location level " @@ -338,6 +345,9 @@ public void getAll(@NotNull Context ctx) { @OpenApiParam(name = EFFECTIVE_DATE, required = true, description = "Specifies " + "the effective date of Location Level to be returned." + "Expected formats are `YYYY-MM-DDTHH:MM` or `YYYY-MM-DDTHH:MM:SS`"), + @OpenApiParam(name = DATE, deprecated = true, description = "Specifies " + + "the effective date of Location Level that will be returned." + + " Deprecated, use " + EFFECTIVE_DATE + " instead"), @OpenApiParam(name = EFFECTIVE_DATE_EXACT, description = "If true" + " only a level with the exact provided date will be returned. If false" + " The most recent level on or before this time will be returned." @@ -369,6 +379,7 @@ public void getAll(@NotNull Context ctx) { description = "Retrieves requested Location Level", tags = TAG ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {EFFECTIVE_DATE}) @Override public void getOne(@NotNull Context ctx, @NotNull String levelId) { String office = requiredParam(ctx, OFFICE); @@ -401,7 +412,10 @@ String.class, null, metrics, name(LevelsController.class.getName(), }, queryParams = { @OpenApiParam(name = EFFECTIVE_DATE, description = "Specifies " - + "the effective date of Location Level that will be updated") + + "the effective date of Location Level that will be updated"), + @OpenApiParam(name = DATE, deprecated = true, description = "Specifies " + + "the effective date of Location Level that will be updated." + + " Deprecated, use " + EFFECTIVE_DATE + " instead") }, requestBody = @OpenApiRequestBody( content = { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java index 791379f34e..f1610e113d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationController.java @@ -73,6 +73,9 @@ public class LocationController implements CrudHandler { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String LOCATIONS_TAG = "Locations"; + private final MetricRegistry metrics; private final Histogram requestResultSize; @@ -144,7 +147,7 @@ private Timer.Context markAndTime(String subject) { }, description = "Returns CWMS Location Data. The Catalog end-point is also capable of " + "retrieving lists of locations and can filter on additional fields.", - tags = {"Locations"} + tags = {LOCATIONS_TAG} ) @Override public void getAll(@NotNull Context ctx) { @@ -155,7 +158,7 @@ public void getAll(@NotNull Context ctx) { LocationsDao locationsDao = getLocationsDao(dsl); String names = ctx.queryParam(NAMES); - String units = ctx.queryParam(UNIT); + String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.SI.value()); String datum = ctx.queryParam(DATUM); String office = ctx.queryParam(OFFICE); @@ -205,6 +208,9 @@ public void getAll(@NotNull Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "The ID of the location to get") + }, queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the location level(s) whose data is to be " @@ -218,6 +224,8 @@ public void getAll(@NotNull Context ctx) { + "default SI units for their parameters."), @OpenApiParam(name = INCLUDE_ALIASES, type = Boolean.class, description = "Specifies whether to " + "include location aliases in the response. Default: false"), + @OpenApiParam(name = DATUM, description = "Specifies the elevation datum of" + + " the response. This field affects only vertical datum. Valid values: NAVD88, NGVD29."), }, responses = { @OpenApiResponse(status = STATUS_200, @@ -229,29 +237,30 @@ public void getAll(@NotNull Context ctx) { + "inputs provided the location was not found.") }, description = "Returns CWMS Location Data", - tags = {"Locations"} + tags = {LOCATIONS_TAG} ) @Override - public void getOne(@NotNull Context ctx, @NotNull String name) { + public void getOne(@NotNull Context ctx, @NotNull String locationId) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { DSLContext dsl = getDslContext(ctx); String units = - ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.EN.value()); + ctx.queryParamAsClass(UNIT, String.class).getOrDefault(UnitSystem.SI.value()); String office = requiredParam(ctx, OFFICE); boolean includeAliases = ctx.queryParamAsClass(INCLUDE_ALIASES, Boolean.class).getOrDefault(false); + String datum = ctx.queryParam(DATUM); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Location.class); ctx.contentType(contentType.toString()); LocationsDao locationDao = getLocationsDao(dsl); - Location location = locationDao.getLocation(name, units, office, includeAliases); + Location location = locationDao.getLocation(locationId, units, office, includeAliases, datum); String serializedLocation = Formats.format(contentType, location); ctx.result(serializedLocation); addDeprecatedContentTypeWarning(ctx, contentType); } catch (IOException ex) { - String errorMsg = "Error retrieving " + name; + String errorMsg = "Error retrieving " + locationId; CdaError re = new CdaError(errorMsg); ctx.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR).json(re); logger.atSevere().withCause(ex).log("%s", errorMsg); @@ -275,7 +284,7 @@ public void getOne(@NotNull Context ctx, @NotNull String name) { description = "Create new CWMS Location", method = HttpMethod.POST, path = "/locations", - tags = {"Locations"} + tags = {LOCATIONS_TAG} ) @Override public void create(@NotNull Context ctx) { @@ -301,6 +310,9 @@ public void create(@NotNull Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "The ID of the location to update") + }, requestBody = @OpenApiRequestBody( content = { @OpenApiContent(from = Location.class, type = Formats.XML), @@ -310,7 +322,7 @@ public void create(@NotNull Context ctx) { description = "Update CWMS Location", method = HttpMethod.PATCH, path = "/locations", - tags = {"Locations"}, + tags = {LOCATIONS_TAG}, responses = { @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + "inputs provided the location was not found.") @@ -329,7 +341,7 @@ public void update(@NotNull Context ctx, @NotNull String locationId) { Location locationFromBody = Formats.parseContent(contentType, ctx.body(), Location.class); //getLocation will throw an error if location does not exist Location existingLocation = locationsDao.getLocation(locationId, - UnitSystem.EN.getValue(), locationFromBody.getOfficeId(), false); + UnitSystem.EN.getValue(), locationFromBody.getOfficeId(), false, null); existingLocation = updatedClearedFields(ctx.body(), contentType.getType(), existingLocation); //only store (update) if location does exist @@ -354,20 +366,21 @@ public void update(@NotNull Context ctx, @NotNull String locationId) { } @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "The ID of the location to delete") + }, queryParams = { @OpenApiParam(name = OFFICE, description = "Specifies the owning office of " + "the location whose data is to be deleted. If this field is not " + "specified, matching location information will be deleted from all " + "offices."), - //Keeping hidden from the API docs for now as this call is particularly destructive - //@OpenApiParam(name = CASCADE_DELETE, type = Boolean.class, - //description = "Specifies whether to specifies whether to delete associated data " + - //"for this location before deleting the location itself. Default: false") + @OpenApiParam(name = CASCADE_DELETE, type = Boolean.class, description = "Specifies whether to delete" + + " associated data for this location before deleting the location itself. Default: false") }, description = "Delete CWMS Location", method = HttpMethod.DELETE, path = "/locations", - tags = {"Locations"}, + tags = {LOCATIONS_TAG}, responses = { @OpenApiResponse(status = STATUS_200, description = "Location successfully deleted from CWMS."), @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java index 48c858a3f0..7d9ffb79b6 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationGroupController.java @@ -86,10 +86,10 @@ private Timer.Context markAndTime(String subject) { + " the assigned locations in the returned location groups. (default: false)"), @OpenApiParam(name = LOCATION_CATEGORY_LIKE, description = "Posix regular expression " + "matching against the location category id"), - @OpenApiParam(name = CATEGORY_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the " + "owning office of the category the location group belongs to " + "whose data is to be included in the response."), - @OpenApiParam(name = LOCATION_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = LOCATION_OFFICE_ID, description = "Specifies the " + "owning office of the location assigned to the location group whose data is to be included in the response."), }, responses = { @@ -108,8 +108,8 @@ public void getAll(@NotNull Context ctx) { DSLContext dsl = getDslContext(ctx); LocationGroupDao cdm = new LocationGroupDao(dsl); - String groupOfficeId = requiredParam(ctx, OFFICE); - String categoryOfficeId = requiredParam(ctx, CATEGORY_OFFICE_ID); + String groupOfficeId = ctx.queryParam(OFFICE); + String categoryOfficeId = ctx.queryParam(CATEGORY_OFFICE_ID); String locationOfficeId = ctx.queryParam(LOCATION_OFFICE_ID); boolean includeAssigned = queryParamAsClass(ctx, new String[]{INCLUDE_ASSIGNED}, @@ -152,11 +152,12 @@ Boolean.class, false, metrics, name(LocationGroupController.class.getName(), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the location group whose data is to be included " + "in the response."), - @OpenApiParam(name = GROUP_OFFICE_ID, required = true, description = "Specifies the " - + "owning office of the location group whose data is to be included in the response."), - @OpenApiParam(name = CATEGORY_OFFICE_ID, required = true, description = "Specifies the " + @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the " + + "owning office of the location group whose data is to be included in the response. " + + "Required for GEO JSON format."), + @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the " + "owning office of the category the location group belongs to " - + "whose data is to be included in the response."), + + "whose data is to be included in the response. Required for GEO JSON format."), @OpenApiParam(name = CATEGORY_ID, required = true, description = "Specifies" + " the category containing the location group whose data is to be " + "included in the response."), @@ -263,6 +264,10 @@ public void create(@NotNull Context ctx) { @OpenApiContent(from = LocationGroup.class, type = Formats.JSON) }, required = true), + pathParams = { + @OpenApiParam(name = GROUP_ID, required = true, description = "Specifies " + + "the location_group to be renamed.") + }, queryParams = { @OpenApiParam(name = REPLACE_ASSIGNED_LOCS, type = Boolean.class, description = "Specifies whether to " + "unassign all existing locations before assigning new locations specified in the content body " @@ -276,7 +281,7 @@ public void create(@NotNull Context ctx) { tags = {TAG} ) @Override - public void update(@NotNull Context ctx, @NotNull String oldGroupId) { + public void update(@NotNull Context ctx, @NotNull String groupId) { try (Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); @@ -288,8 +293,8 @@ public void update(@NotNull Context ctx, @NotNull String oldGroupId) { boolean replaceAssignedLocs = ctx.queryParamAsClass(REPLACE_ASSIGNED_LOCS, Boolean.class).getOrDefault(false); LocationGroupDao locationGroupDao = new LocationGroupDao(dsl); - if (!office.equalsIgnoreCase(CWMS_OFFICE) && !oldGroupId.equals(deserialize.getId())) { - locationGroupDao.renameLocationGroup(oldGroupId, deserialize); + if (!office.equalsIgnoreCase(CWMS_OFFICE) && !groupId.equals(deserialize.getId())) { + locationGroupDao.renameLocationGroup(groupId, deserialize); } if (replaceAssignedLocs) { locationGroupDao.unassignAllLocs(deserialize, office); @@ -321,8 +326,8 @@ public void delete(@NotNull Context ctx, @NotNull String groupId) { DSLContext dsl = getDslContext(ctx); LocationGroupDao dao = new LocationGroupDao(dsl); - String office = ctx.queryParam(OFFICE); - String categoryId = ctx.queryParam(CATEGORY_ID); + String office = requiredParam(ctx, OFFICE); + String categoryId = requiredParam(ctx, CATEGORY_ID); boolean cascadeDelete = ctx.queryParamAsClass(CASCADE_DELETE, Boolean.class).getOrDefault(false); dao.delete(categoryId, groupId, cascadeDelete, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LocationKindController.java b/cwms-data-api/src/main/java/cwms/cda/api/LocationKindController.java index f61bf96f31..2b5bc56a45 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LocationKindController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LocationKindController.java @@ -103,9 +103,8 @@ public void handle(@NotNull Context ctx) { String kindRegexMask = ctx.queryParam(LOCATION_KIND_LIKE); String office = ctx.queryParam(OFFICE); - String formatParm = ctx.queryParamAsClass(Formats.JSONV1, String.class).getOrDefault(""); String formatHeader = ctx.header(Header.ACCEPT); - ContentType contentType = Formats.parseHeaderAndQueryParm(formatHeader, formatParm, CwmsIdLocationKind.class); + ContentType contentType = Formats.parseHeader(formatHeader, CwmsIdLocationKind.class); String results; diff --git a/cwms-data-api/src/main/java/cwms/cda/api/LookupTypeController.java b/cwms-data-api/src/main/java/cwms/cda/api/LookupTypeController.java index 1390398626..f6a5cc4f54 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/LookupTypeController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/LookupTypeController.java @@ -27,6 +27,7 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.LookupTypeDao; import cwms.cda.data.dto.LookupType; import cwms.cda.data.dto.StatusResponse; @@ -51,23 +52,12 @@ import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -public final class LookupTypeController implements CrudHandler { +public final class LookupTypeController extends BaseCrudHandler { static final String TAG = "LookupTypes"; - private final MetricRegistry metrics; - - private final Histogram requestResultSize; - public LookupTypeController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -99,16 +89,14 @@ public void getAll(Context ctx) { String serialized = Formats.format(contentType, lookupTypes, LookupType.class); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); - requestResultSize.update(serialized.length()); + updateResultSize(serialized.length()); } } @OpenApi(ignore = true) @Override public void getOne(@NotNull Context context, @NotNull String s) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + context.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -146,6 +134,9 @@ public void create(Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location type to update.") + }, queryParams = { @OpenApiParam(name = CATEGORY, required = true, description = "Specifies the category id of the lookup type to be updated."), @OpenApiParam(name = PREFIX, required = true, description = "Specifies the prefix of the lookup type to be updated."), @@ -164,6 +155,7 @@ public void create(Context ctx) { ) @Override public void update(Context ctx, String name) { + logUnusedPathParameter(ctx, NAME, "Body has required information"); String category = requiredParam(ctx, CATEGORY); String prefix = requiredParam(ctx, PREFIX); try (Timer.Context ignored = markAndTime(UPDATE)) { @@ -180,6 +172,9 @@ public void update(Context ctx, String name) { } @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location type to delete.") + }, queryParams = { @OpenApiParam(name = CATEGORY, required = true, description = "Specifies the category id of the lookup type to be deleted."), @OpenApiParam(name = PREFIX, required = true, description = "Specifies the prefix of the lookup type to be deleted."), diff --git a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java index 9c3cc445f6..92b48e6e91 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/MeasurementController.java @@ -56,6 +56,7 @@ import static cwms.cda.api.Controllers.queryParamAsInstant; import static cwms.cda.api.Controllers.requiredParam; import cwms.cda.api.enums.UnitSystem; +import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.MeasurementDao; import cwms.cda.data.dto.StatusResponse; import cwms.cda.data.dto.measurement.Measurement; @@ -165,10 +166,7 @@ public void getAll(@NotNull Context ctx) { @OpenApi(ignore = true) @Override public void getOne(@NotNull Context ctx, @NotNull String locationId) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } - + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -208,9 +206,7 @@ public void create(Context ctx) { @OpenApi(ignore = true) @Override public void update(@NotNull Context ctx, @NotNull String locationId) { - try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -220,9 +216,9 @@ public void update(@NotNull Context ctx, @NotNull String locationId) { }, queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of the measurements to delete"), - @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window to delete. " + + @OpenApiParam(name = BEGIN, description = "The start of the time window to delete. " + TIME_FORMAT_DESC), - @OpenApiParam(name = END, required = true, description = "The end of the time window to delete." + + @OpenApiParam(name = END, description = "The end of the time window to delete." + TIME_FORMAT_DESC), @OpenApiParam(name = TIMEZONE, description = "This field specifies a default timezone " + "to be used if the format of the " + BEGIN + "and " + END diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java b/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java index dcd9f09182..b3a103d451 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ParametersController.java @@ -52,13 +52,13 @@ private Timer.Context markAndTime(String subject) { @OpenApi(ignore = true) @Override public void create(Context ctx) { - ctx.status(HttpServletResponse.SC_NOT_FOUND); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void delete(Context ctx, String id) { - ctx.status(HttpServletResponse.SC_NOT_FOUND); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @@ -67,8 +67,8 @@ public void delete(Context ctx, String id) { @OpenApiParam(name = FORMAT, deprecated = true, description = "Specifies the" + " encoding format of the response. Valid value for the format field" + " for this URI are:" - + "\n* `tab`" - + "\n* `csv`" + + "\n* `tab` (deprecated)" + + "\n* `csv` (deprecated)" + "\n* `xml`" + "\n* `json` (default)" + "\n\nSee this page for more information about accept header usage."), @@ -131,16 +131,13 @@ public void getAll(Context ctx) { @OpenApi(ignore = true) @Override public void getOne(Context ctx, String id) { - try (final Timer.Context timeContext = markAndTime(GET_ONE)) { - ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void update(Context ctx, String id) { ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); - } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java index 5d7616031f..6905608bfd 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PoolController.java @@ -14,6 +14,7 @@ import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.PAGE; import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.PARAMETER_ID; import static cwms.cda.api.Controllers.POOL_ID; import static cwms.cda.api.Controllers.PROJECT_ID; import static cwms.cda.api.Controllers.RESULTS; @@ -23,6 +24,7 @@ import static cwms.cda.api.Controllers.STATUS_501; import static cwms.cda.api.Controllers.TOP_MASK; import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; @@ -83,6 +85,11 @@ private Timer.Context markAndTime(String subject) { + " in the request you are. This is an opaque value, and can be" + " obtained from the 'next-page' value in the response." ), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = @@ -185,8 +192,8 @@ public void getOne(@NotNull Context ctx, @NotNull String poolId) { PoolDao dao = new PoolDao(dsl); // These are required - String office = ctx.queryParam(OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String office = requiredParam(ctx, OFFICE);; + String projectId = requiredParam(ctx, PROJECT_ID); // These are optional String bottomMask = @@ -226,18 +233,18 @@ public void getOne(@NotNull Context ctx, @NotNull String poolId) { @OpenApi(ignore = true) @Override public void create(@NotNull Context ctx) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void update(@NotNull Context ctx, @NotNull String locationCode) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void delete(@NotNull Context ctx, @NotNull String locationCode) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java index 62c92d9e75..c5e592d768 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/ProjectController.java @@ -231,8 +231,8 @@ public void create(@NotNull Context ctx) { @OpenApiParam(name = NAME, description = "The name of the project to be renamed"), }, queryParams = { - @OpenApiParam(name = NAME, description = "The new name of the project"), - @OpenApiParam(name = OFFICE, description = "The office of the project to be renamed"), + @OpenApiParam(name = NAME, required = true, description = "The new name of the project"), + @OpenApiParam(name = OFFICE, required = true, description = "The office of the project to be renamed"), }, requestBody = @OpenApiRequestBody( content = { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java index 7a2459a1e6..78707d30ad 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/PropertyController.java @@ -52,31 +52,18 @@ import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -public final class PropertyController implements CrudHandler { +public final class PropertyController extends BaseCrudHandler { static final String TAG = "Properties"; - private final MetricRegistry metrics; - - private final Histogram requestResultSize; - public PropertyController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( - pathParams = { - }, queryParams = { @OpenApiParam(name = OFFICE_MASK, description = "Filters properties to the specified office mask"), - @OpenApiParam(name = CATEGORY_ID, description = "Filters properties to the specified category mask"), + @OpenApiParam(name = CATEGORY_ID_MASK, description = "Filters properties to the specified category mask"), @OpenApiParam(name = NAME_MASK, description = "Filters properties to the specified name mask"), }, responses = { @@ -105,7 +92,7 @@ public void getAll(Context ctx) { String serialized = Formats.format(contentType, properties, Property.class); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); - requestResultSize.update(serialized.length()); + updateResultSize(serialized.length()); } } @@ -149,7 +136,7 @@ public void getOne(Context ctx, String name) { String serialized = Formats.format(contentType, property); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); - requestResultSize.update(serialized.length()); + updateResultSize(serialized.length()); } } @@ -184,6 +171,10 @@ public void create(Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the name of the property to be " + + "updated."), + }, requestBody = @OpenApiRequestBody( content = { @OpenApiContent(from = Property.class, type = Formats.JSON) @@ -198,6 +189,7 @@ public void create(Context ctx) { ) @Override public void update(Context ctx, String name) { + logUnusedPathParameter(ctx, NAME, "Body contains required information"); try (Timer.Context ignored = markAndTime(UPDATE)) { String formatHeader = ctx.req.getContentType(); ContentType contentType = Formats.parseHeader(formatHeader, Property.class); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/RangeParser.java b/cwms-data-api/src/main/java/cwms/cda/api/RangeParser.java new file mode 100644 index 0000000000..ccfb4e2047 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/RangeParser.java @@ -0,0 +1,128 @@ +package cwms.cda.api; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.regex.*; + +/** + * Utility class for parsing HTTP Range headers. + * These typically look like: bytes=100-1234 + * or: bytes=100- this is common to resume a download + * or: bytes=0- equivalent to a regular request for the whole file + * but by returning 206 we show that we support range requests + * Note that multiple ranges can be requested at once such + * as: bytes=500-600,700-999 Server responds identifies separator and then puts separator between chunks + * bytes=0-0,-1 also legal its just the first and the last byte + * or: bytes=500-600,601-999 legal but what is the point? + * or: bytes=500-700,601-999 legal, notice they overlap. + * + * + */ +public class RangeParser { + + private static final Pattern RANGE_PATTERN = Pattern.compile("(\\d*)-(\\d*)"); + + /** + * Return a list of two element long[] containing byte ranges parsed from the HTTP Range header. + * If the end of a range is not specified ( e.g. bytes=100- ) then a -1 is returned in the second position + * If the range only includes a negative byte (e.g bytes=-50) then -1 is returned as the start of the range + * and -1*end is returned as the end of the range. bytes=-50 will result in [-1,50] + * + * @param header the HTTP Range header this should start with "bytes=" if it is null or empty an empty list is returned + * @return a list of long[2] holding the ranges + */ + public static List parse(String header) { + if (header == null || header.isEmpty() ) { + return Collections.emptyList(); + } else if ( !header.startsWith("bytes=")){ + throw new IllegalArgumentException("Invalid Range header: " + header); + } + + String rangePart = header.substring(6); + List retval = parseRanges(rangePart); + if( retval.isEmpty() ){ + throw new IllegalArgumentException("Invalid Range header: " + header); + } + return retval; + } + + public static long[] parseFirstRange(String header) { + if(header != null) { + List ranges = RangeParser.parse(header); + if (!ranges.isEmpty()) { + return ranges.get(0); + } + } + return null; + } + + public static @NotNull List parseRanges(String rangePart) { + if( rangePart == null || rangePart.isEmpty() ){ + throw new IllegalArgumentException("Invalid range specified: " + rangePart); + } + String[] parts = rangePart.split(","); + List ranges = new ArrayList<>(); + + for (String part : parts) { + Matcher m = RANGE_PATTERN.matcher(part.trim()); + if (m.matches()) { + String start = m.group(1); + String end = m.group(2); + + long s = start.isEmpty() ? -1 : Long.parseLong(start); + long e = end.isEmpty() ? -1 : Long.parseLong(end); + + ranges.add(new long[]{s, e}); + } + } + return ranges; + } + + /** + * The parse() method in this class can return -1 for unspecified values or when suffix ranges are supplied. + * This method interprets the negative values in regard to the totalSize and returns inclusive indices of the + * requested range. + * @param inputs the array of start and end byte positions + * @param totalBytes the total number of bytes in the file + * @return a long array with the start and end byte positions, these are inclusive. [0,0] means return the first byte + */ + public static long[] interpret(long[] inputs, long totalBytes){ + if(inputs == null){ + throw new IllegalArgumentException("null range array provided"); + } else if( inputs.length != 2 ){ + throw new IllegalArgumentException("Invalid number of inputs: " + Arrays.toString(inputs)); + } + + long start = inputs[0]; + long end = inputs[1]; + + if(start == -1L){ + // its a suffix request. + start = totalBytes - end; + end = totalBytes - 1; + } else { + if (start < 0 ) { + throw new IllegalArgumentException("Invalid range specified: " + Arrays.toString(inputs)); + } + + if(end == -1L){ + end = totalBytes - 1; + } + + if(end < start){ + throw new IllegalArgumentException("Invalid range specified: " + Arrays.toString(inputs)); + } + + if(start > totalBytes - 1){ + throw new IllegalArgumentException("Can't satisfy range request: " + Arrays.toString(inputs) + " Range starts beyond end of file."); + } + + end = Math.min(end, totalBytes - 1); + } + + return new long[]{start, end}; + } + + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/RangeRequestUtil.java b/cwms-data-api/src/main/java/cwms/cda/api/RangeRequestUtil.java index c84c74afac..97cdbc6f0a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/RangeRequestUtil.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/RangeRequestUtil.java @@ -1,61 +1,78 @@ package cwms.cda.api; +import com.google.common.flogger.FluentLogger; import io.javalin.core.util.Header; import io.javalin.http.Context; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Arrays; +import java.io.Reader; import java.util.List; +import org.apache.commons.io.IOUtils; public class RangeRequestUtil { + static FluentLogger logger = FluentLogger.forEnclosingClass(); private RangeRequestUtil() { // utility class } + + public static void seekableStream(Context ctx, InputStream is, String mediaType, long totalBytes) throws IOException { + seekableStream(ctx, is, 0, mediaType, totalBytes); + } + /** - * Javalin has a method very similar to this in its Context class. The issue is that Javalin decided to - * take the InputStream, wrap it in a CompletedFuture and then process the request asynchronously. This - * causes problems when the InputStream is tied to a database connection that gets closed before the - * async processing happens. This method doesn't do the async thing but tries to support the rest. - * @param ctx - * @param is - * @param mediaType - * @param totalBytes - * @throws IOException + * This method copies data from InputStream is and into the Context response OutputStream. + * If the request include a Range request header than the response will be a partial response for the first range. + * Javalin has a similar method in its Context class that handles the streaming asynchronously. The Javalin + * method caused problems when our database connections were closed before the streaming completes. + * Both SQL Blobs and S3 streams are capable of retrieving Streams at a user-controlled offset. Those methods + * should be used and the offset passed in the isPosition parameter. + * If additional skipping needs to be done, this method will call InputStream.skip() which may have some + * implications for specific implementations but works efficiently for others. + * @param ctx the Javalin context + * @param is the input stream + * @param isPostion the current position in the input stream. + * @param mediaType the content type + * @param totalBytes the total number of bytes in the input stream + * @throws IOException if either of the streams throw an IOException */ - public static void seekableStream(Context ctx, InputStream is, String mediaType, long totalBytes) throws IOException { - long from = 0; - long to = totalBytes - 1; + public static void seekableStream(Context ctx, InputStream is, long isPostion, String mediaType, long totalBytes) throws IOException { + if (ctx.header(Header.RANGE) == null) { + // Not a range request. ctx.res.setContentType(mediaType); + + if(isPostion > 0){ + throw new IllegalArgumentException("Input stream position must be 0 for non-range requests"); + } + // Javalin's version of this method doesn't set the content-length // Not setting the content-length makes the servlet container use Transfer-Encoding=chunked. // Chunked is a worse experience overall, seems like we should just set the length if we know it. - writeRange(ctx.res.getOutputStream(), is, from, Math.min(to, totalBytes - 1)); + ctx.header(Header.CONTENT_LENGTH, String.valueOf(totalBytes)); + + IOUtils.copyLarge(is, (OutputStream) ctx.res.getOutputStream(), 0, totalBytes); } else { - int chunkSize = 128000; String rangeHeader = ctx.header(Header.RANGE); - String[] eqSplit = rangeHeader.split("=", 2); - String[] dashSplit = eqSplit[1].split("-", -1); // keep empty trailing part - - List requestedRange = Arrays.stream(dashSplit) - .filter(s -> !s.isEmpty()) - .collect(java.util.stream.Collectors.toList()); - - from = Long.parseLong(requestedRange.get(0)); - - if (from + chunkSize > totalBytes) { - // chunk bigger than file, write all - to = totalBytes - 1; - } else if (requestedRange.size() == 2) { - // chunk smaller than file, to/from specified - to = Long.parseLong(requestedRange.get(1)); - } else { - // chunk smaller than file, to/from not specified - to = from + chunkSize - 1; + + List ranges = RangeParser.parse(rangeHeader); + + long[] requestedRange = ranges.get(0); + if( ranges.size() > 1 ){ + // we support range requests but we not currently supporting multiple ranges. + // Range request are optional so we have choices what to do if multiple ranges are requested: + // We could return 416 and hope the client figures out to only send one range + // We could service the first range with 206 and ignore the other ranges + // We could ignore the range request entirely and return the full body with 200 + // We could implement support for multiple ranges + logger.atInfo().log("Multiple ranges requested, using first and ignoring additional ranges"); } + requestedRange = RangeParser.interpret(requestedRange, totalBytes); + + long from = requestedRange[0]; + long to = requestedRange[1]; ctx.status(206); @@ -64,33 +81,98 @@ public static void seekableStream(Context ctx, InputStream is, String mediaType, ctx.res.setContentType(mediaType); ctx.header(Header.CONTENT_LENGTH, String.valueOf(Math.min(to - from + 1, totalBytes))); - writeRange(ctx.res.getOutputStream(), is, from, Math.min(to, totalBytes - 1)); + + if(isPostion < from){ + skip(is, from-isPostion); + } + long len = Math.min(to, totalBytes - 1) - from + 1; + + // If the inputOffset to IOUtils.copyLarge is not 0 then IOUtils will do its own skipping. For reasons + // that IOUtils explains (quirks of certain streams) it does its skipping via read(). Using read() has + // performance implications b/c all the skipped data still gets retrieved and copied to memory. In our + // use-case the data comes from a database blob/clob/s3. Copying (potential) gigabytes of data we + // don't need across the network is not ideal. We've tried to address this performance impact in two + // ways. 1) We allow callers to pass an input stream that is already positioned so that skipping isn't + // needed. 2) If skipping is needed we call InputStream.skip() directly (this is efficient for the + // stream from Oracle Blobs). + + // We do our own skipping and then have IOUtils copy. + IOUtils.copyLarge(is, (OutputStream) ctx.res.getOutputStream(), 0, len); } } + /** + * Similar to seekableStream but for Reader. For some reason the java.sql.Clob does not specify a method to get a + * Stream that is already positioned but there is a method to position a Reader. + * @param ctx + * @param reader + * @param isPostion + * @param mediaType + * @param totalBytes + * @throws IOException + */ + public static void seekableReader(Context ctx, Reader reader, long isPostion, String mediaType, long totalBytes) throws IOException { + + if (ctx.header(Header.RANGE) == null) { + // Not a range request. + ctx.res.setContentType(mediaType); + + if(isPostion > 0){ + throw new IllegalArgumentException("Input stream position must be 0 for non-range requests"); + } + + ctx.header(Header.CONTENT_LENGTH, String.valueOf(totalBytes)); + + IOUtils.copyLarge(reader, ctx.res.getWriter(), 0, totalBytes); + } else { + String rangeHeader = ctx.header(Header.RANGE); + + List ranges = RangeParser.parse(rangeHeader); + + long[] requestedRange = ranges.get(0); + if( ranges.size() > 1 ){ + // we support range requests but we not currently supporting multiple ranges. + // Range request are optional so we have choices what to do if multiple ranges are requested: + // We could return 416 and hope the client figures out to only send one range + // We could service the first range with 206 and ignore the other ranges + // We could ignore the range request entirely and return the full body with 200 + // We could implement support for multiple ranges + logger.atInfo().log("Multiple ranges requested, using first and ignoring additional ranges"); + } + requestedRange = RangeParser.interpret(requestedRange, totalBytes); + + long from = requestedRange[0]; + long to = requestedRange[1]; + + ctx.status(206); + + ctx.header(Header.ACCEPT_RANGES, "bytes"); + ctx.header(Header.CONTENT_RANGE, "bytes " + from + "-" + to + "/" + totalBytes); + + ctx.res.setContentType(mediaType); + ctx.header(Header.CONTENT_LENGTH, String.valueOf(Math.min(to - from + 1, totalBytes))); + + if(isPostion < from){ + skip(reader, from-isPostion); + } + long len = Math.min(to, totalBytes - 1) - from + 1; - public static void writeRange(OutputStream out, InputStream in, long from, long to) throws IOException { - writeRange(out, in, from, to, new byte[8192]); + IOUtils.copyLarge(reader, ctx.res.getWriter(), 0, len); + } } - public static void writeRange(OutputStream out, InputStream is, long from, long to, byte[] buffer) throws IOException { - long toSkip = from; + private static void skip(InputStream is, long toSkip) throws IOException { while (toSkip > 0) { long skipped = is.skip(toSkip); toSkip -= skipped; } + } - long bytesLeft = to - from + 1; - while (bytesLeft != 0L) { - int maxRead = (int) Math.min(buffer.length, bytesLeft); - int read = is.read(buffer, 0, maxRead); - if (read == -1) { - break; - } - out.write(buffer, 0, read); - bytesLeft -= read; + private static void skip(Reader reader, long toSkip) throws IOException { + while (toSkip > 0) { + long skipped = reader.skip(toSkip); + toSkip -= skipped; } - } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java b/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java index 1686f6e635..31638a5d43 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/SpecifiedLevelController.java @@ -122,8 +122,7 @@ public void getAll(Context ctx) { @OpenApi(ignore = true) @Override public void getOne(Context ctx, String templateId) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -164,7 +163,7 @@ public void create(Context ctx) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the specified level to be renamed"), - @OpenApiParam(name = SPECIFIED_LEVEL_ID, description = "The new specified level id.") + @OpenApiParam(name = SPECIFIED_LEVEL_ID, required = true, description = "The new specified level id.") }, method = HttpMethod.PATCH, tags = {TAG} @@ -175,8 +174,8 @@ public void update(Context ctx, @NotNull String oldSpecifiedLevelId) { DSLContext dsl = getDslContext(ctx); SpecifiedLevelDao dao = getDao(dsl); - String newSpecifiedLevelId = ctx.queryParam(SPECIFIED_LEVEL_ID); - String office = ctx.queryParam(OFFICE); + String newSpecifiedLevelId = requiredParam(ctx, SPECIFIED_LEVEL_ID); + String office = requiredParam(ctx, OFFICE); dao.update(oldSpecifiedLevelId, newSpecifiedLevelId, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); } @@ -202,7 +201,7 @@ public void delete(Context ctx, String specifiedLevelId) { DSLContext dsl = getDslContext(ctx); SpecifiedLevelDao dao = getDao(dsl); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); dao.delete(specifiedLevelId, office); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java b/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java index 79e06dadf1..bdf3d737a5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/StandardTextController.java @@ -26,6 +26,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.texttimeseries.StandardTextDao; @@ -76,7 +77,9 @@ private Timer.Context markAndTime(String subject) { @OpenApiParam(name = OFFICE_MASK, description = "Specifies the office filter of the" + "standard text."), @OpenApiParam(name = STANDARD_TEXT_ID_MASK, description = "Specifies the text id filter of the " - + "standard text") + + "standard text"), + @OpenApiParam(name = NAME_MASK, deprecated = true, description = "Specifies the text id filter of the " + + "standard text. Deprecated, use " + STANDARD_TEXT_ID_MASK) }, responses = { @OpenApiResponse(status = STATUS_200, @@ -94,10 +97,7 @@ public void getAll(Context ctx) { if (officeMask == null) { officeMask = "*"; } - String idMask = ctx.queryParam(NAME_MASK); - if (idMask == null) { - idMask = "*"; - } + String idMask = queryParamAsClass(ctx, new String[]{STANDARD_TEXT_ID_MASK, NAME_MASK}, String.class, "*"); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, StandardTextCatalog.class); DSLContext dsl = getDslContext(ctx); @@ -133,10 +133,7 @@ public void getAll(Context ctx) { @Override public void getOne(@NotNull Context ctx, @NotNull String stdTextId) { try (Timer.Context ignored = markAndTime(DELETE)) { - String office = ctx.queryParam(OFFICE); - if (office == null) { - throw new IllegalArgumentException(OFFICE + " is a required parameter"); - } + String office = requiredParam(ctx, OFFICE); DSLContext dsl = getDslContext(ctx); StandardTextValue standardTextValue = getDao(dsl).retrieveStandardText(stdTextId, office); @@ -182,7 +179,7 @@ public void create(@NotNull Context ctx) { @OpenApi(ignore = true) @Override public void update(@NotNull Context ctx, @NotNull String oldTextTimeSeriesId) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @@ -204,12 +201,8 @@ public void update(@NotNull Context ctx, @NotNull String oldTextTimeSeriesId) { @Override public void delete(@NotNull Context ctx, @NotNull String stdTextId) { try (Timer.Context ignored = markAndTime(DELETE)) { - String office = ctx.queryParam(OFFICE); - if (office == null) { - throw new IllegalArgumentException(OFFICE + " is a required parameter"); - } - JooqDao.DeleteMethod deleteMethod = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class) - .getOrThrow(e -> new IllegalArgumentException(METHOD + " is a required parameter")); + String office = requiredParam(ctx, OFFICE); + JooqDao.DeleteMethod deleteMethod = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); String deleteAction; switch (deleteMethod) { case DELETE_ALL: diff --git a/cwms-data-api/src/main/java/cwms/cda/api/StreamLocationController.java b/cwms-data-api/src/main/java/cwms/cda/api/StreamLocationController.java index 3e3f4813c0..8c3f84ad97 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/StreamLocationController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/StreamLocationController.java @@ -71,21 +71,12 @@ import static cwms.cda.data.dao.JooqDao.getDslContext; -public final class StreamLocationController implements CrudHandler { +public final class StreamLocationController extends BaseCrudHandler { static final String TAG = "StreamLocations"; - private final MetricRegistry metrics; - private final Histogram requestResultSize; - public StreamLocationController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -128,7 +119,7 @@ public void getAll(Context ctx) { String serialized = Formats.format(contentType, streamLocations, StreamLocation.class); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); - requestResultSize.update(serialized.length()); + updateResultSize(serialized.length()); } } @@ -174,7 +165,7 @@ public void getOne(@NotNull Context ctx, @NotNull String locationId) { String serialized = Formats.format(contentType, streamLocation); ctx.result(serialized); ctx.status(HttpServletResponse.SC_OK); - requestResultSize.update(serialized.length()); + updateResultSize(serialized.length()); } } @@ -212,6 +203,10 @@ public void create(Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = NAME, required = true, description = "Specifies the location-id of " + + "the stream location to be renamed."), + }, requestBody = @OpenApiRequestBody( content = { @OpenApiContent(from = StreamLocation.class, type = Formats.JSONV1) @@ -225,7 +220,8 @@ public void create(Context ctx) { } ) @Override - public void update(Context ctx, @NotNull String locationId) { + public void update(Context ctx, @NotNull String name) { + logUnusedPathParameter(ctx, NAME, "Body contains required information"); try (Timer.Context ignored = markAndTime(METHOD + "update")) { String formatHeader = ctx.req.getContentType(); ContentType contentType = Formats.parseHeader(formatHeader, StreamLocation.class); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesController.java index 4e557631e3..43893dd3a9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesController.java @@ -56,7 +56,7 @@ -public class TextTimeSeriesController implements CrudHandler { +public class TextTimeSeriesController extends BaseCrudHandler { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); static final String TAG = "Text-TimeSeries"; @@ -65,10 +65,8 @@ public class TextTimeSeriesController implements CrudHandler { public static final boolean DEFAULT_CREATE_REPLACE_ALL = false; public static final boolean DEFAULT_UPDATE_REPLACE_ALL = true; - private final MetricRegistry metrics; - public TextTimeSeriesController(MetricRegistry metrics) { - this.metrics = metrics; + super(metrics); } @NotNull @@ -76,12 +74,6 @@ protected TimeSeriesTextDao getDao(DSLContext dsl) { return new TimeSeriesTextDao(dsl); } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); - } - - @OpenApi( summary = "Retrieve text time series values for a provided time window and date version." + "If individual values exceed 64 kilobytes, a URL to a separate download is provided " @@ -95,6 +87,8 @@ private Timer.Context markAndTime(String subject) { + "the time zone of the values of the begin and end fields (unless " + "otherwise specified). If this field is not specified, " + "the default time zone of UTC shall be used."), + @OpenApiParam(name = VERSION_DATE, description = "Specifies the version date of the " + + "text timeseries. If not specified, the latest version will be used."), @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window"), @OpenApiParam(name = END, required = true, description = "The end of the time window.") }, @@ -162,7 +156,7 @@ public void getAll(@NotNull Context ctx) { @OpenApi(ignore = true) @Override public void getOne(@NotNull Context ctx, @NotNull String templateId) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -217,7 +211,7 @@ public void create(@NotNull Context ctx) { ) @Override public void update(@NotNull Context ctx, @NotNull String oldTextTimeSeriesId) { - + logUnusedPathParameter(ctx, NAME, "Body contains required information"); try (Timer.Context ignored = markAndTime(UPDATE)) { boolean replaceAll = ctx.queryParamAsClass(REPLACE_ALL, Boolean.class) .getOrDefault(DEFAULT_UPDATE_REPLACE_ALL); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java index 72857b8bd5..1f3cd343cf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TextTimeSeriesValueController.java @@ -27,8 +27,9 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.ClobDao; +import cwms.cda.data.dao.StreamConsumer; +import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.OpenApi; @@ -37,26 +38,16 @@ import io.javalin.plugin.openapi.annotations.OpenApiResponse; import org.jooq.DSLContext; -import javax.servlet.http.HttpServletResponse; -import java.io.InputStream; - import static com.codahale.metrics.MetricRegistry.name; import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -public class TextTimeSeriesValueController implements Handler { +public class TextTimeSeriesValueController extends BaseHandler { public static final String TEXT_PLAIN = "text/plain"; - private final MetricRegistry metrics; - private final Histogram requestResultSize; public TextTimeSeriesValueController(MetricRegistry metrics) { - this.metrics = metrics; - requestResultSize = this.metrics.histogram((name(TextTimeSeriesValueController.class, RESULTS, SIZE))); - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -67,14 +58,9 @@ private Timer.Context markAndTime(String subject) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office of " + "the Text TimeSeries whose data is to be included in the response."), - @OpenApiParam(name = TIMEZONE, description = "Specifies " - + "the time zone of the values of the begin and end fields (unless " - + "otherwise specified). If this field is not specified, " - + "the default time zone of UTC shall be used."), - @OpenApiParam(name = DATE, required = true, description = "The date of the text value to retrieve"), - @OpenApiParam(name = VERSION_DATE, description = "The version date for the value to retrieve."), @OpenApiParam(name = CLOB_ID, description = "Will be removed in a schema update. " + - "This is a placeholder for integration testing with schema 23.3.16", deprecated = true) + "This is a placeholder for integration testing with schema 23.3.16", deprecated = true, + required = true) }, responses = { @OpenApiResponse(status = STATUS_200, @@ -86,24 +72,21 @@ private Timer.Context markAndTime(String subject) { ) public void handle(Context ctx) { //Implementation will change with new CWMS schema - //https://www.hec.usace.army.mil/confluence/display/CWMS/2024-02-29+Task2A+Text-ts+and+Binary-ts+Design + //https://www.hec.usace.army.mil/confluence/spaces/CWMS/pages/183110112/2024-02-29+Developer+Meeting+Task2A+Text-ts+and+Binary-ts+Design + logUnusedPathParameter(ctx, NAME, "Handled as " + CLOB_ID + " in query parameter. May change with schema."); String textId = requiredParam(ctx, CLOB_ID); String officeId = requiredParam(ctx, OFFICE); try (Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); ClobDao clobDao = new ClobDao(dsl); - clobDao.getClob(textId, officeId, clob -> { - if (clob == null) { - ctx.status(HttpServletResponse.SC_NOT_FOUND).json(new CdaError("Unable to find " - + "clob based on given parameters")); - } else { - long size = clob.length(); - requestResultSize.update(size); - try(InputStream is = clob.getAsciiStream()){ - RangeRequestUtil.seekableStream(ctx, is, TEXT_PLAIN, size); - } - } - }); + + StreamConsumer consumer = (is, isPosition, mediaType, totalLength) -> { + updateResultSize(totalLength); + ctx.header(Header.ACCEPT_RANGES, "bytes"); + RangeRequestUtil.seekableStream(ctx, is, isPosition, mediaType, totalLength); + }; + clobDao.getClob(textId, officeId, consumer); + } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesCategoryController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesCategoryController.java index e7f455ae75..c3e981f03d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesCategoryController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesCategoryController.java @@ -31,6 +31,7 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.TimeSeriesCategoryDao; import cwms.cda.data.dto.TimeSeriesCategory; @@ -47,7 +48,6 @@ import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.util.List; import java.util.Optional; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -76,17 +76,18 @@ private Timer.Context markAndTime(String subject) { + "timeseries category(ies) whose data is to be included in the response. If " + "this field is not specified, matching timeseries category information from" + " all offices shall be returned."),}, - responses = {@OpenApiResponse(status = STATUS_200, + responses = { + @OpenApiResponse(status = STATUS_200, content = {@OpenApiContent(isArray = true, from = TimeSeriesCategory.class, type = Formats.JSON) }), - @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " - + "inputs provided the categories were not found."), - @OpenApiResponse(status = STATUS_501, description = "request format is not " - + "implemented")}, description = "Returns CWMS timeseries category " - + "Data", tags = {TAG}) + @OpenApiResponse(status = STATUS_404, description = "Based on the combination of " + + "inputs provided the categories were not found."), + @OpenApiResponse(status = STATUS_501, description = "request format is not " + + "implemented")}, description = "Returns CWMS timeseries category " + + "Data", tags = {TAG}) @Override - public void getAll(Context ctx) { + public void getAll(@NotNull Context ctx) { try (final Timer.Context timeContext = markAndTime(GET_ALL)){ DSLContext dsl = getDslContext(ctx); @@ -131,7 +132,7 @@ public void getAll(Context ctx) { + "implemented")}, description = "Retrieves requested timeseries category", tags = {TAG}) @Override - public void getOne(Context ctx, @NotNull String categoryId) { + public void getOne(@NotNull Context ctx, @NotNull String categoryId) { try (final Timer.Context timeContext = markAndTime(GET_ONE)){ DSLContext dsl = getDslContext(ctx); @@ -154,9 +155,7 @@ public void getOne(Context ctx, @NotNull String categoryId) { logger.atInfo().log("%s%nfor request %s", re, ctx.fullUrl()); ctx.status(HttpServletResponse.SC_NOT_FOUND).json(re); } - } - } @OpenApi( @@ -169,12 +168,14 @@ public void getOne(Context ctx, @NotNull String categoryId) { queryParams = { @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, description = "Create will fail if provided ID already exists. Default: true"), + @OpenApiParam(name = IGNORE_NULLS, type = Boolean.class, + description = "Ignore null values in the request body. Default: true") }, method = HttpMethod.POST, tags = {TAG} ) @Override - public void create(Context ctx) { + public void create(@NotNull Context ctx) { try (Timer.Context ignored = markAndTime(CREATE)){ DSLContext dsl = getDslContext(ctx); @@ -184,16 +185,48 @@ public void create(Context ctx) { ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesCategory.class); TimeSeriesCategory deserialize = Formats.parseContent(contentType, body, TimeSeriesCategory.class); boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); + boolean ignoreNulls = ctx.queryParamAsClass(IGNORE_NULLS, Boolean.class).getOrDefault(true); TimeSeriesCategoryDao dao = new TimeSeriesCategoryDao(dsl); - dao.create(deserialize, failIfExists); + dao.create(deserialize, failIfExists, ignoreNulls); ctx.status(HttpServletResponse.SC_CREATED); } } - @OpenApi(ignore = true) + @OpenApi( + description = "Update existing TimeSeriesCategory. Allows for renaming of the category.", + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = TimeSeriesCategory.class, type = Formats.JSON) + }, + required = true), + pathParams = { + @OpenApiParam(name = CATEGORY_ID, required = true, description = "Specifies " + + "the original timeseries category to rename.") + }, + queryParams = { + @OpenApiParam(name = IGNORE_NULLS, type = Boolean.class, + description = "Ignore null values in the request body. Default: true") + + }, + method = HttpMethod.PATCH, + tags = {TAG} + ) @Override - public void update(@NotNull Context ctx, @NotNull String locationCode) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + public void update(@NotNull Context ctx, @NotNull String categoryId) { + try (Timer.Context ignored = markAndTime(UPDATE)) { + DSLContext dsl = getDslContext(ctx); + + String formatHeader = ctx.req.getContentType(); + String body = ctx.body(); + + boolean ignoreNulls = ctx.queryParamAsClass(IGNORE_NULLS, Boolean.class).getOrDefault(true); + ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesCategory.class); + TimeSeriesCategory deserialize = Formats.parseContent(contentType, body, TimeSeriesCategory.class); + + TimeSeriesCategoryDao dao = new TimeSeriesCategoryDao(dsl); + dao.update(categoryId, deserialize, ignoreNulls); + ctx.status(HttpServletResponse.SC_OK); + } } @OpenApi( @@ -211,8 +244,8 @@ public void update(@NotNull Context ctx, @NotNull String locationCode) { tags = {TAG} ) @Override - public void delete(Context ctx, @NotNull String categoryId) { - try (Timer.Context ignored = markAndTime(UPDATE)){ + public void delete(@NotNull Context ctx, @NotNull String categoryId) { + try (Timer.Context ignored = markAndTime(UPDATE)) { DSLContext dsl = getDslContext(ctx); TimeSeriesCategoryDao dao = new TimeSeriesCategoryDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java index 45e463879c..6a7841ff27 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesController.java @@ -178,11 +178,6 @@ private Timer.Context markAndTime(String subject) { required = true ), queryParams = { - @OpenApiParam(name = TIMEZONE, description = "Specifies " - + "the time zone of the version-date field (unless " - + "otherwise specified). If this field is not specified, the default time zone " - + "of UTC shall be used.\r\nIgnored if version-date was specified with " - + "offset and timezone."), @OpenApiParam(name = CREATE_AS_LRTS, type = Boolean.class, description = "Flag indicating if " + "timeseries should be created as Local Regular Time Series. " + "'True' or 'False', default is 'False'"), @@ -417,6 +412,11 @@ public void delete(@NotNull Context ctx, @NotNull String timeseries) { + "of data as a series of pages. This parameter is used to describes the " + "current location in the response stream. This is an opaque " + "value, and can be obtained from the 'next-page' value in the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. " @@ -580,7 +580,7 @@ private void addLinkHeader(@NotNull Context ctx, TimeSeries ts, ContentType cont public void getOne(@NotNull Context ctx, @NotNull String id) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } @@ -597,11 +597,6 @@ public void getOne(@NotNull Context ctx, @NotNull String id) { }, required = true), queryParams = { - @OpenApiParam(name = TIMEZONE, description = "Specifies " - + "the time zone of the version-date field (unless " - + "otherwise specified). If this field is not specified, the default time zone " - + "of UTC shall be used.\r\nIgnored if version-date was specified with " - + "offset and timezone."), @OpenApiParam(name = CREATE_AS_LRTS, type = Boolean.class, description = ""), @OpenApiParam(name = STORE_RULE, type = StoreRule.class, description = STORE_RULE_DESC), @OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java index 48d339ca80..3393c23898 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesFilteredController.java @@ -141,6 +141,11 @@ private TimeSeriesDao getTimeSeriesDao(DSLContext dsl) { + "of data as a series of pages. This parameter is used to describes the " + "current location in the response stream. This is an opaque " + "value, and can be obtained from the 'next-page' value in the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. " @@ -209,7 +214,7 @@ public void handle(@NotNull Context ctx) { ? DateUtils.parseUserDate(end, timezone) : ZonedDateTime.now(tz); - String office = requiredParam(ctx, OFFICE); + String office = ctx.queryParam(OFFICE); FilteredTimeSeriesParameters ftsParams = FilteredTimeSeriesParameters.Builder.from(ctx) .build(); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java index 9152953f6e..43f937582b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesGroupController.java @@ -25,28 +25,7 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.CATEGORY_ID; -import static cwms.cda.api.Controllers.CATEGORY_OFFICE_ID; -import static cwms.cda.api.Controllers.CREATE; -import static cwms.cda.api.Controllers.CWMS_OFFICE; -import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.GET_ONE; -import static cwms.cda.api.Controllers.GROUP_ID; -import static cwms.cda.api.Controllers.GROUP_OFFICE_ID; -import static cwms.cda.api.Controllers.INCLUDE_ASSIGNED; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.REPLACE_ASSIGNED_TS; -import static cwms.cda.api.Controllers.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_404; -import static cwms.cda.api.Controllers.STATUS_501; -import static cwms.cda.api.Controllers.TIMESERIES_CATEGORY_LIKE; -import static cwms.cda.api.Controllers.TIMESERIES_GROUP_LIKE; -import static cwms.cda.api.Controllers.UPDATE; -import static cwms.cda.api.Controllers.queryParamAsClass; -import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; @@ -97,6 +76,8 @@ private Timer.Context markAndTime(String subject) { @OpenApiParam(name = OFFICE, description = "Specifies the owning office of the " + "timeseries assigned to the group(s) whose data is to be included in the response. If this " + "field is not specified, group information for all assigned TS offices shall be returned."), + @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the owning office of the " + + "timeseries group"), @OpenApiParam(name = INCLUDE_ASSIGNED, type = Boolean.class, description = "Include" + " the assigned timeseries in the returned timeseries groups. (default: true)"), @OpenApiParam(name = TIMESERIES_CATEGORY_LIKE, description = "Posix regular expression " @@ -161,15 +142,15 @@ Boolean.class, true, metrics, name(TimeSeriesGroupController.class.getName(), + "the timeseries group whose data is to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the timeseries assigned to the group whose data is to be included" + " in the response. This will limit the assigned timeseries returned to only those" + " assigned to the specified office."), @OpenApiParam(name = CATEGORY_OFFICE_ID, description = "Specifies the owning office of the " - + "timeseries group category", required = true), + + "timeseries group category"), @OpenApiParam(name = GROUP_OFFICE_ID, description = "Specifies the owning office of the " - + "timeseries group", required = true), - @OpenApiParam(name = CATEGORY_ID, required = true, description = "Specifies" + + "timeseries group"), + @OpenApiParam(name = CATEGORY_ID, description = "Specifies" + " the category containing the timeseries group whose data is to be " + "included in the response."), }, @@ -195,26 +176,11 @@ public void getOne(@NotNull Context ctx, @NotNull String groupId) { String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesGroup.class); - TimeSeriesGroup group = null; - List timeSeriesGroups = dao.getTimeSeriesGroups(tsOffice, groupOffice, categoryOffice, - categoryId, groupId); - if (timeSeriesGroups != null && !timeSeriesGroups.isEmpty()) { - if (timeSeriesGroups.size() == 1) { - group = timeSeriesGroups.get(0); - } else { - // An error. [office, categoryId, groupId] should have, at most, one match - String message = String.format( - "Multiple TimeSeriesGroups returned from getTimeSeriesGroups " - + "for:%s category:%s groupId:%s At most one match was " - + "expected. Found:%s", - groupOffice, categoryId, groupId, timeSeriesGroups); - throw new IllegalArgumentException(message); - } - } + TimeSeriesGroup group = dao.getTimeSeriesGroup(tsOffice, groupOffice, categoryOffice, categoryId, groupId); + if (group != null) { String result = Formats.format(contentType, group); - ctx.result(result); ctx.contentType(contentType.toString()); requestResultSize.update(result.length()); @@ -240,6 +206,16 @@ public void getOne(@NotNull Context ctx, @NotNull String groupId) { queryParams = { @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, description = "Create will fail if provided ID already exists. Default: true"), + @OpenApiParam(name = IGNORE_NULLS, type = Boolean.class, + description = "Ignore null values in the request body. Caution, if " + FAIL_IF_EXISTS + + " is false and " + IGNORE_NULLS + " is false, then the create will proceed whether " + + "there was an existing group or not. If there was an existing group with a " + + "description and the provided body does not specify a description (its null) the " + + "combination of flags will cause the database to replace the description with null. " + + "If " + IGNORE_NULLS + " is false and the provided body does not specify the " + + "list of assigned time series this will result in the database replacing the list " + + "with an empty list." + + "Default: true") }, method = HttpMethod.POST, tags = {TAG} @@ -261,9 +237,10 @@ public void create(@NotNull Context ctx) { + "TimeSeries Category office ID"); } + boolean ignoreNulls = ctx.queryParamAsClass(IGNORE_NULLS, Boolean.class).getOrDefault(true); boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(true); TimeSeriesGroupDao dao = new TimeSeriesGroupDao(dsl); - dao.create(deserialize, failIfExists); + dao.create(deserialize, failIfExists, ignoreNulls); ctx.status(HttpServletResponse.SC_CREATED); } } @@ -276,6 +253,10 @@ public void create(@NotNull Context ctx) { @OpenApiContent(from = TimeSeriesGroup.class, type = Formats.JSON) }, required = true), + pathParams = { + @OpenApiParam(name = GROUP_ID, required = true, description = "Specifies " + + "the original timeseries group to rename.") + }, queryParams = { @OpenApiParam(name = REPLACE_ASSIGNED_TS, type = Boolean.class, description = "Specifies whether to " + "unassign all existing time series before assigning new time series specified in the content body " @@ -296,17 +277,17 @@ public void update(@NotNull Context ctx, @NotNull String oldGroupId) { String body = ctx.body(); String office = requiredParam(ctx, OFFICE); ContentType contentType = Formats.parseHeader(formatHeader, TimeSeriesGroup.class); - TimeSeriesGroup deserialize = Formats.parseContent(contentType, body, TimeSeriesGroup.class); + TimeSeriesGroup group = Formats.parseContent(contentType, body, TimeSeriesGroup.class); boolean replaceAssignedTs = ctx.queryParamAsClass(REPLACE_ASSIGNED_TS, Boolean.class) .getOrDefault(false); TimeSeriesGroupDao timeSeriesGroupDao = new TimeSeriesGroupDao(dsl); - if (!office.equalsIgnoreCase(CWMS_OFFICE) && !oldGroupId.equals(deserialize.getId())) { - timeSeriesGroupDao.renameTimeSeriesGroup(oldGroupId, deserialize); + if (!office.equalsIgnoreCase(CWMS_OFFICE) && !oldGroupId.equals(group.getId())) { + timeSeriesGroupDao.renameTimeSeriesGroup(oldGroupId, group); } if (replaceAssignedTs) { - timeSeriesGroupDao.unassignAllTs(deserialize, office); + timeSeriesGroupDao.unassignForOffice(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), office); } - timeSeriesGroupDao.assignTs(deserialize, office); + timeSeriesGroupDao.assignTs(group, office); ctx.status(HttpServletResponse.SC_OK); } } @@ -321,6 +302,8 @@ public void update(@NotNull Context ctx, @NotNull String oldGroupId) { + "time series category of the time series group to be deleted"), @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + "owning office of the time series group to be deleted"), + @OpenApiParam(name = CASCADE_DELETE, type = Boolean.class, + description = "Specifies whether to unassign time series in this group before deleting. Default: false"), }, method = HttpMethod.DELETE, tags = {TAG} @@ -331,9 +314,11 @@ public void delete(@NotNull Context ctx, @NotNull String groupId) { DSLContext dsl = getDslContext(ctx); TimeSeriesGroupDao dao = new TimeSeriesGroupDao(dsl); - String office = ctx.queryParam(OFFICE); - String categoryId = ctx.queryParam(CATEGORY_ID); - dao.delete(categoryId, groupId, office); + + boolean cascadeDelete = ctx.queryParamAsClass(CASCADE_DELETE, Boolean.class).getOrDefault(false); + String office = requiredParam(ctx, OFFICE); + String categoryId = requiredParam(ctx, CATEGORY_ID); + dao.delete(categoryId, groupId, office, cascadeDelete); ctx.status(HttpServletResponse.SC_NO_CONTENT); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java index 5081e6ab21..142a430c66 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeSeriesIdentifierDescriptorController.java @@ -180,7 +180,7 @@ public void getOne(@NotNull Context ctx, @NotNull String timeseriesId) { DSLContext dsl = getDslContext(ctx); TimeSeriesIdentifierDescriptorDao dao = new TimeSeriesIdentifierDescriptorDao(dsl); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); String formatHeader = ctx.header(Header.ACCEPT); if (Formats.DEFAULT.equals(formatHeader)) { @@ -329,7 +329,7 @@ public void update(@NotNull Context ctx, @NotNull String name) { @Override public void delete(@NotNull Context ctx, @NotNull String timeseriesId) { - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method =requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); String office = requiredParam(ctx, OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java b/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java index bdbdecc42b..44cf6d9ff3 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TimeZoneController.java @@ -17,6 +17,7 @@ import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.TimeZoneDao; import cwms.cda.data.dto.TimeZoneId; import cwms.cda.data.dto.TimeZoneIds; @@ -54,13 +55,13 @@ private Timer.Context markAndTime(String subject) { @OpenApi(ignore = true) @Override public void create(Context ctx) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void delete(Context ctx, String id) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -130,15 +131,13 @@ public void getAll(Context ctx) { @OpenApi(ignore = true) @Override public void getOne(Context ctx, String id) { - try (Timer.Context timeContext = markAndTime(GET_ONE)) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); - } + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void update(Context ctx, String id) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java index 6fd640da3b..15cbf4ca9c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesDeleteController.java @@ -25,47 +25,23 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.BEGIN; -import static cwms.cda.api.Controllers.DELETE; -import static cwms.cda.api.Controllers.END; -import static cwms.cda.api.Controllers.END_TIME_INCLUSIVE; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.NAME; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.OVERRIDE_PROTECTION; -import static cwms.cda.api.Controllers.PAGE_SIZE; -import static cwms.cda.api.Controllers.PROJECT_ID; -import static cwms.cda.api.Controllers.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.START_TIME_INCLUSIVE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_204; -import static cwms.cda.api.Controllers.STATUS_404; -import static cwms.cda.api.Controllers.UNIT_SYSTEM; -import static cwms.cda.api.Controllers.requiredInstant; -import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import cwms.cda.api.enums.UnitSystem; import cwms.cda.data.dao.location.kind.TurbineDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.StatusResponse; -import cwms.cda.data.dto.location.kind.TurbineChange; -import cwms.cda.formatters.ContentType; -import cwms.cda.formatters.Formats; -import io.javalin.core.util.Header; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; -import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.time.Instant; -import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -96,6 +72,11 @@ private Timer.Context markAndTime(String subject) { "turbine changes to be deleted."), }, queryParams = { + @OpenApiParam(name = TIMEZONE, description = "Specifies " + + "the time zone of the values of " + BEGIN + ", " + END + " fields (unless " + + "otherwise specified). If this field is not specified, the default time zone " + + "of UTC shall be used.\r\nIgnored if " + BEGIN + " was specified with " + + "offset and timezone."), @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window"), @OpenApiParam(name = END, required = true, description = "The end of the time window."), @OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = "A flag " @@ -111,6 +92,7 @@ private Timer.Context markAndTime(String subject) { + "inputs provided the project was not found.") } ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) public void handle(@NotNull Context ctx) throws Exception { String projectId = ctx.pathParam(NAME); String office = ctx.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java index d9d5a98370..870608d0e1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesGetController.java @@ -25,48 +25,25 @@ package cwms.cda.api; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.BEGIN; -import static cwms.cda.api.Controllers.CREATE; -import static cwms.cda.api.Controllers.DELETE; -import static cwms.cda.api.Controllers.END; -import static cwms.cda.api.Controllers.END_TIME_INCLUSIVE; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.NAME; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.OVERRIDE_PROTECTION; -import static cwms.cda.api.Controllers.PAGE_SIZE; -import static cwms.cda.api.Controllers.PROJECT_ID; -import static cwms.cda.api.Controllers.RESULTS; -import static cwms.cda.api.Controllers.SIZE; -import static cwms.cda.api.Controllers.START_TIME_INCLUSIVE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_204; -import static cwms.cda.api.Controllers.STATUS_404; -import static cwms.cda.api.Controllers.UNIT_SYSTEM; -import static cwms.cda.api.Controllers.requiredInstant; -import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.enums.UnitSystem; -import cwms.cda.api.errors.CdaError; -import cwms.cda.api.errors.RequiredQueryParameterException; import cwms.cda.data.dao.location.kind.TurbineDao; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.location.kind.TurbineChange; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import io.javalin.apibuilder.CrudHandler; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; -import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; -import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.time.Instant; import java.util.List; @@ -100,6 +77,11 @@ private Timer.Context markAndTime(String subject) { "Turbine changes whose data is to be included in the response."), }, queryParams = { + @OpenApiParam(name = TIMEZONE, description = "Specifies " + + "the time zone of the values of " + BEGIN + ", " + END + " fields (unless " + + "otherwise specified). If this field is not specified, the default time zone " + + "of UTC shall be used.\r\nIgnored if " + BEGIN + " was specified with " + + "offset and timezone."), @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window"), @OpenApiParam(name = END, required = true, description = "The end of the time window."), @OpenApiParam(name = START_TIME_INCLUSIVE, type = Boolean.class, description = "A flag " @@ -130,6 +112,7 @@ private Timer.Context markAndTime(String subject) { description = "Returns matching CWMS Turbine Change Data for a Reservoir Project.", tags = {TurbineController.TAG} ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) public void handle(@NotNull Context ctx) throws Exception { String projectId = ctx.pathParam(NAME); String office = ctx.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesPostController.java b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesPostController.java index dda5a6dd06..533f930446 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesPostController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/TurbineChangesPostController.java @@ -75,16 +75,11 @@ import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -public final class TurbineChangesPostController implements Handler { - private final MetricRegistry metrics; +public final class TurbineChangesPostController extends BaseHandler { public TurbineChangesPostController(MetricRegistry metrics) { - this.metrics = metrics; - } - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + super(metrics); } @OpenApi( @@ -115,6 +110,9 @@ private Timer.Context markAndTime(String subject) { ) @Override public void handle(@NotNull Context ctx) throws Exception { + logUnusedPathParameter(ctx, NAME, "Body contains required information."); + logUnusedPathParameter(ctx, OFFICE, "Body contains required information."); + try (Timer.Context ignored = markAndTime(CREATE)) { String formatHeader = ctx.req.getContentType(); ContentType contentType = Formats.parseHeader(formatHeader, TurbineChange.class); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java new file mode 100644 index 0000000000..e3479cc1c4 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/VerticalDatumController.java @@ -0,0 +1,236 @@ +/* + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.api; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; +import static com.codahale.metrics.MetricRegistry.name; +import static cwms.cda.api.Controllers.CREATE; +import static cwms.cda.api.Controllers.DELETE; +import static cwms.cda.api.Controllers.GET_ONE; +import static cwms.cda.api.Controllers.LOCATION_ID; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.RESULTS; +import static cwms.cda.api.Controllers.SIZE; +import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.api.Controllers.UPDATE; +import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.data.dao.JooqDao.getDslContext; + +import cwms.cda.api.errors.CdaError; +import cwms.cda.data.dao.VerticalDatumDao; +import cwms.cda.data.dto.StatusResponse; +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import io.javalin.apibuilder.CrudHandler; +import io.javalin.core.util.Header; +import io.javalin.http.Context; +import io.javalin.plugin.openapi.annotations.HttpMethod; +import io.javalin.plugin.openapi.annotations.OpenApi; +import io.javalin.plugin.openapi.annotations.OpenApiContent; +import io.javalin.plugin.openapi.annotations.OpenApiParam; +import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; +import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import javax.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.jooq.DSLContext; + +import static cwms.cda.api.LocationController.LOCATIONS_TAG; + +public final class VerticalDatumController implements CrudHandler { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + // NOTE: manually expanded due to limits of OpenApi Annotations. + private static final String VDI_PATH = "/location/{location-id}/vertical-datum"; + private final MetricRegistry metrics; + private final Histogram requestResultSize; + + public VerticalDatumController(MetricRegistry metrics) { + this.metrics = metrics; + String className = this.getClass().getName(); + requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); + } + + private Timer.Context markAndTime(String subject) { + return Controllers.markAndTime(metrics, getClass().getName(), subject); + } + + @Override + public void getAll(@NotNull Context ctx) { + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "Specifies the location-id.") + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office."), + @OpenApiParam(name = UNIT, description = "Specifies the unit of measure for elevation/offsets (e.g., m or ft). Default is m.") + }, + responses = { + @OpenApiResponse(status = Controllers.STATUS_200, + content = {@OpenApiContent(type = Formats.JSONV1, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.JSON, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.XMLV1, from = VerticalDatumInfo.class), + @OpenApiContent(type = Formats.XML, from = VerticalDatumInfo.class)}) + }, + description = "Returns Vertical Datum Info for the specified location.", + path = VDI_PATH, + tags = {LOCATIONS_TAG} + ) + @Override + public void getOne(@NotNull Context ctx, @NotNull String locationId) { + String office = requiredParam(ctx, OFFICE); + String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault("m"); + try (Timer.Context ignored = markAndTime(GET_ONE)) { + DSLContext dsl = getDslContext(ctx); + VerticalDatumDao dao = new VerticalDatumDao(dsl); + VerticalDatumInfo info = dao.retrieveVerticalDatumInfo(office, locationId, units); + String formatHeader = ctx.header(Header.ACCEPT); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); + ctx.contentType(contentType.toString()); + String serialized = Formats.format(contentType, info); + ctx.result(serialized); + ctx.status(HttpServletResponse.SC_OK); + requestResultSize.update(serialized.length()); + } + } + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1), + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1) + }, + required = true), + queryParams = { + @OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location id for this vertical-datum-info."), + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office.") + }, + description = "Create Vertical Datum Info for a Location", + method = HttpMethod.POST, + path = VDI_PATH, + tags = {LOCATIONS_TAG}, + responses = { + @OpenApiResponse(status = Controllers.STATUS_201, description = "Vertical Datum Info successfully stored to CWMS.") + } + ) + @Override + public void create(@NotNull Context ctx) { + try (Timer.Context ignored = markAndTime(CREATE)) { + String formatHeader = ctx.req.getContentType(); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); + VerticalDatumInfo info = Formats.parseContent(contentType, ctx.body(), VerticalDatumInfo.class); + //allow locationId and office to be specified in either the body or as query params, but require them to be present in one of those places + String locationId = info.getLocation(); + String office = info.getOffice(); + if(locationId == null || locationId.isBlank()) { + locationId = requiredParam(ctx, LOCATION_ID); + } + if(office == null || office.isBlank()) { + office = requiredParam(ctx, OFFICE); + } + DSLContext dsl = getDslContext(ctx); + VerticalDatumDao dao = new VerticalDatumDao(dsl); + dao.createVerticalDatumInfo(office, locationId, info); + StatusResponse re = new StatusResponse(office, + "Vertical Datum Info successfully stored to CWMS.", locationId); + ctx.status(HttpServletResponse.SC_CREATED).json(re); + } + } + + @OpenApi( + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.JSONV1), + @OpenApiContent(from = VerticalDatumInfo.class, type = Formats.XMLV1) + }, + required = true), + pathParams = { + @OpenApiParam(name = LOCATION_ID, description = "The ID of the location to update") + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office.") + }, + description = "Update Vertical Datum Info for a Location", + method = HttpMethod.PATCH, + path = VDI_PATH, + tags = {LOCATIONS_TAG}, + responses = { + @OpenApiResponse(status = Controllers.STATUS_200, description = "Updated Vertical Datum Info") + } + ) + @Override + public void update(@NotNull Context ctx, @NotNull String locationId) { + try (Timer.Context ignored = markAndTime(UPDATE)) { + String formatHeader = ctx.req.getContentType(); + ContentType contentType = Formats.parseHeader(formatHeader, VerticalDatumInfo.class); + VerticalDatumInfo info = Formats.parseContent(contentType, ctx.body(), VerticalDatumInfo.class); + //allow locationId and office to be specified in either the body or as query params, but require them to be present in one of those places + String office = info.getOffice(); + if(office == null || office.isBlank()) { + office = requiredParam(ctx, OFFICE); + } + DSLContext dsl = getDslContext(ctx); + VerticalDatumDao dao = new VerticalDatumDao(dsl); + dao.updateVerticalDatumInfo(office, locationId, info); + StatusResponse re = new StatusResponse(office, + "Updated Vertical Datum Info", locationId); + ctx.status(HttpServletResponse.SC_OK).json(re); + } + } + + @OpenApi( + pathParams = { + @OpenApiParam(name = LOCATION_ID, required = true, description = "Specifies the location-id for the vertical-datum being deleted.") + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the owning office.") + }, + description = "Delete Vertical Datum Info for a Location", + method = HttpMethod.DELETE, + path = VDI_PATH, + tags = {LOCATIONS_TAG}, + responses = { + @OpenApiResponse(status = Controllers.STATUS_200, description = "Vertical Datum Info successfully deleted from CWMS."), + @OpenApiResponse(status = Controllers.STATUS_404, description = "Vertical Datum Info not found.") + } + ) + @Override + public void delete(@NotNull Context ctx, @NotNull String locationId) { + String office = requiredParam(ctx, OFFICE); + try (Timer.Context ignored = markAndTime(DELETE)) { + DSLContext dsl = getDslContext(ctx); + VerticalDatumDao dao = new VerticalDatumDao(dsl); + dao.deleteVerticalDatumInfo(office, locationId); + StatusResponse re = new StatusResponse(office, + "Vertical Datum Info successfully deleted from CWMS.", locationId); + ctx.status(HttpServletResponse.SC_OK).json(re); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java b/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java index 279c0843a6..59329d5731 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/auth/ApiKeyController.java @@ -46,6 +46,7 @@ import io.javalin.plugin.openapi.annotations.OpenApiSecurity; import java.util.List; +import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -97,6 +98,10 @@ public void create(Context ctx) { } @OpenApi( + pathParams = { + @OpenApiParam(name = "key-name", required = true, + description = "Name of the specific key to get more information for. NOTE: Case-sensitive.") + }, responses = @OpenApiResponse( content = { @OpenApiContent(from = ApiKey.class, type = Formats.JSON) @@ -180,7 +185,7 @@ public void getOne(Context ctx, @NotNull String keyName) { ) @Override public void update(@NotNull Context ctx, @NotNull String arg1) { - throw new UnsupportedOperationException("Update is not implemented. Delete and create a new key."); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/auth/users/UsersController.java b/cwms-data-api/src/main/java/cwms/cda/api/auth/users/UsersController.java index e23602162d..3b9fe43814 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/auth/users/UsersController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/auth/users/UsersController.java @@ -1,48 +1,30 @@ package cwms.cda.api.auth.users; import static com.codahale.metrics.MetricRegistry.name; -import static cwms.cda.api.Controllers.CURSOR; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.INCLUDE_VALUES; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.PAGE; -import static cwms.cda.api.Controllers.PAGE_SIZE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_201; -import static cwms.cda.api.Controllers.STATUS_204; -import static cwms.cda.api.Controllers.markAndTime; -import static cwms.cda.api.Controllers.queryParamAsClass; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; -import java.util.List; - +import javax.servlet.http.HttpServletResponse; import org.jooq.DSLContext; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import cwms.cda.ApiServlet; -import cwms.cda.api.ClobController; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.UserDao; -import cwms.cda.data.dto.Clobs; import cwms.cda.data.dto.CwmsDTOPaginated; -import cwms.cda.data.dto.auth.ApiKey; import cwms.cda.data.dto.auth.users.User; import cwms.cda.data.dto.auth.users.Users; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; -import cwms.cda.security.Role; import io.javalin.apibuilder.CrudHandler; -import io.javalin.core.security.RouteRole; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.HttpCode; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; -import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; import io.javalin.plugin.openapi.annotations.OpenApiSecurity; @@ -63,31 +45,42 @@ private Timer.Context markAndTime(String subject) { @OpenApi(ignore = true) @Override public void create(Context ctx) { - throw new UnsupportedOperationException("Unimplemented method 'create'"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void delete(Context ctx, String username) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'delete'"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( queryParams = { - @OpenApiParam(allowEmptyValue = true, name = OFFICE, type = String.class, + @OpenApiParam(allowEmptyValue = true, name = OFFICE, description = "Show only users with active privileges in a given office." + Controllers.OFFICE_DESCRIPTION ), + @OpenApiParam(name = USERNAME_LIKE, + description = "Posix regular expression " + + " matching against the username"), @OpenApiParam(name = PAGE, description = "This end point can return a lot of data, this " + "identifies where in the request you are. This is an opaque" + " value, and can be obtained from the 'next-page' value in " + "the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. Default " - + DEFAULT_PAGE_SIZE + ".") + + DEFAULT_PAGE_SIZE + "."), + @OpenApiParam(name = INCLUDE_ROLES, + type = Boolean.class, + allowEmptyValue = true, + description = "Include roles in the response. Default false.") }, responses = @OpenApiResponse( content = { @@ -106,6 +99,7 @@ public void getAll(Context ctx) { try (final Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); String office = ctx.queryParam(OFFICE); + String usernameRegex = ctx.queryParam(USERNAME_LIKE); String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, Users.class); @@ -122,11 +116,11 @@ public void getAll(Context ctx) { int pageSize = queryParamAsClass(ctx, new String[]{PAGE_SIZE}, Integer.class, DEFAULT_PAGE_SIZE, metrics, name(UsersController.class.getName(), GET_ALL)); - boolean includeRoles = queryParamAsClass(ctx, new String[]{"include-roles"}, + boolean includeRoles = queryParamAsClass(ctx, new String[]{INCLUDE_ROLES}, Boolean.class, false, metrics, name(UsersController.class.getName(), GET_ALL)); UserDao dao = new UserDao(dsl); - Users users = dao.getAll(cursor, pageSize, office, includeRoles); + Users users = dao.getAll(cursor, pageSize, office, includeRoles, usernameRegex); String result = Formats.format(contentType, users); @@ -170,9 +164,6 @@ public void getOne(Context ctx, String userName) { ) @Override public void update(Context ctx, String arg1) { - throw new UnsupportedOperationException("Unimplemented method 'update'"); + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } - - - } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/enums/MessageQueue.java b/cwms-data-api/src/main/java/cwms/cda/api/enums/MessageQueue.java new file mode 100644 index 0000000000..8fd9cf30e1 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/enums/MessageQueue.java @@ -0,0 +1,42 @@ +package cwms.cda.api.enums; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + name = "Message Queue", + description = "Desired set of status messages. Must be one of the named options." + + " Letter casing is ignored." +) +public enum MessageQueue { + TS_STORED("TS_STORED", " CWMS messages about time series operations, such as data stored and deleted"), + STATUS("STATUS", " CWMS general system and application status messages"), + REALTIME_OPS("REALTIME_OPS", " CWMS application operational messages"); + + private String queue; + private String description; + + MessageQueue(String queue, String description) { + this.queue = queue; + this.description = description; + } + + public String value() { + return queue; + } + + public String description() { + return description; + } + + public static MessageQueue queueFor(String queue) { + if (TS_STORED.value().equalsIgnoreCase(queue)) { + return TS_STORED; + } else if (STATUS.value().equalsIgnoreCase(queue)) { + return STATUS; + } else if (REALTIME_OPS.value().equalsIgnoreCase(queue)) { + return REALTIME_OPS; + } else { + return null; + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java new file mode 100644 index 0000000000..1be35d52ea --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/NoDataRateException.java @@ -0,0 +1,21 @@ +package cwms.cda.api.errors; + +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.logging.Level; + +public class NoDataRateException extends RateException{ + // 404 is Not Found - this isn't quite right b/c the rate() or reverse-rate() isn't failing b/c + // the ts isn't found its failing b/c a necessary rating table isn't found. + // 424 is Failed Dependency - this is closer to what we want, but it's not quite right either. + + // 422 is Unprocessable Entity. I think this is the closest. + + public static final int HTTP_ERROR_CODE = 422; + + public NoDataRateException(String message, SQLException cause) { + super(message, DATABASE_SOURCE, "Error performing rate function: " + message, + HTTP_ERROR_CODE, Level.INFO, new LinkedHashMap<>(), cause); + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java b/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java index 6e0c3f0096..69504204ce 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/errors/RateException.java @@ -24,16 +24,23 @@ package cwms.cda.api.errors; +import java.io.Serializable; import java.sql.SQLException; -import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.logging.Level; import javax.servlet.http.HttpServletResponse; -public final class RateException extends ApplicationException { +public class RateException extends ApplicationException { private static final Level LOG_LEVEL = Level.INFO; + public RateException(String message, String source, String cdaErrorMessage, int cdaHttpErrorCode, + Level logLevel, Map details, Throwable cause) { + super(message, source, cdaErrorMessage, cdaHttpErrorCode, logLevel, details, cause); + } + public RateException(String message, SQLException cause) { super(message, DATABASE_SOURCE, "Error performing rate function: " + message, - HttpServletResponse.SC_INTERNAL_SERVER_ERROR, LOG_LEVEL, new HashMap<>(), cause); + HttpServletResponse.SC_INTERNAL_SERVER_ERROR, LOG_LEVEL, new LinkedHashMap<>(), cause); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java index 0d27aee916..b8c793e327 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeDeleteController.java @@ -20,24 +20,24 @@ package cwms.cda.api.location.kind; +import static cwms.cda.api.Controllers.*; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.location.kind.OutletDao; import cwms.cda.data.dto.CwmsId; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.http.Context; import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.sql.Timestamp; import java.time.Instant; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static cwms.cda.api.Controllers.*; -import static cwms.cda.api.Controllers.GET_ALL; public class GateChangeDeleteController extends BaseHandler { @@ -53,6 +53,10 @@ public GateChangeDeleteController(MetricRegistry metrics) { "Gate Changes whose data is to be included in the response."), }, queryParams = { + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default " + + "timezone to be used if the format of the " + + BEGIN + " and " + END + " parameters do not include " + + "offset or time zone information. Defaults to UTC."), @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window"), @OpenApiParam(name = END, required = true, description = "The end of the time window."), @OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = "A flag " @@ -66,6 +70,7 @@ public GateChangeDeleteController(MetricRegistry metrics) { tags = {OutletController.TAG}, method = HttpMethod.DELETE ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context context) throws Exception { String office = context.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java index c8226be942..fdc6cfc972 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/GateChangeGetAllController.java @@ -20,6 +20,8 @@ package cwms.cda.api.location.kind; +import static cwms.cda.api.Controllers.*; + import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; @@ -30,19 +32,18 @@ import cwms.cda.data.dto.location.kind.GateChange; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.sql.Timestamp; import java.time.Instant; import java.util.List; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -import static cwms.cda.api.Controllers.*; public class GateChangeGetAllController extends BaseHandler { private static final int DEFAULT_PAGE_SIZE = 500; @@ -59,6 +60,10 @@ public GateChangeGetAllController(MetricRegistry metrics) { "Gate Changes whose data is to be included in the response."), }, queryParams = { + @OpenApiParam(name = TIMEZONE, description = "This field specifies a default " + + "timezone to be used if the format of the " + + BEGIN + " and " + END + " parameters do not include " + + "offset or time zone information. Defaults to UTC."), @OpenApiParam(name = BEGIN, required = true, description = "The start of the time window"), @OpenApiParam(name = END, required = true, description = "The end of the time window."), @OpenApiParam(name = START_TIME_INCLUSIVE, type = Boolean.class, description = "A flag " @@ -90,6 +95,7 @@ public GateChangeGetAllController(MetricRegistry metrics) { description = "Returns matching CWMS gate change data for a Reservoir Project.", tags = {OutletController.TAG} ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context context) { String office = context.pathParam(OFFICE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java index 1acb7e586c..5475598789 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/location/kind/LockController.java @@ -108,7 +108,7 @@ private Timer.Context markAndTime(String subject) { public void getAll(@NotNull Context ctx) { try (Timer.Context ignored = markAndTime(GET_ALL)) { String office = requiredParam(ctx, OFFICE); - String projectId = ctx.queryParam(PROJECT_ID); + String projectId = requiredParam(ctx, PROJECT_ID); CwmsId project = CwmsId.buildCwmsId(office, projectId); DSLContext dsl = getDslContext(ctx); LockDao dao = new LockDao(dsl); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockRevoke.java b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockRevoke.java index 43f6eeb16a..ef13e45483 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockRevoke.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectLockRevoke.java @@ -66,6 +66,7 @@ public ProjectLockRevoke(MetricRegistry metrics) { queryParams = { @OpenApiParam(name = OFFICE, required = true, description = "Specifies the office of the lock."), + @OpenApiParam(name = APPLICATION_ID, required = true, description = "Specifies the application id."), @OpenApiParam(name = REVOKE_TIMEOUT, type = Integer.class, description = "time in seconds to wait for existing lock to be revoked. Default: 10") }, diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectPublishStatusUpdate.java b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectPublishStatusUpdate.java index 77ebc7eeda..1f94272a73 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectPublishStatusUpdate.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/ProjectPublishStatusUpdate.java @@ -24,23 +24,13 @@ package cwms.cda.api.project; -import static cwms.cda.api.Controllers.APPLICATION_ID; -import static cwms.cda.api.Controllers.BEGIN; -import static cwms.cda.api.Controllers.END; -import static cwms.cda.api.Controllers.NAME; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.SOURCE_ID; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.TIMESERIES_ID; -import static cwms.cda.api.Controllers.queryParamAsInstant; -import static cwms.cda.api.Controllers.requiredParam; - import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.Controllers; import cwms.cda.api.ProjectController; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dao.project.ProjectDao; +import cwms.cda.formatters.Formats; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.HttpMethod; @@ -50,6 +40,7 @@ import java.time.Instant; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; +import static cwms.cda.api.Controllers.*; public class ProjectPublishStatusUpdate implements Handler { @@ -80,6 +71,13 @@ public ProjectPublishStatusUpdate(MetricRegistry metrics) { @OpenApiParam(name = TIMESERIES_ID, description = "A time series identifier of " + "the time series associated with the update. If NULL or not " + "specified, the generated message will not include this item."), + @OpenApiParam(name = TIMEZONE, description = "Specifies " + + "the time zone of the values of the begin and end fields (unless " + + "otherwise specified). For other formats this parameter " + + "affects the time zone of times in the " + + "response. If this field is not specified, the default time zone " + + "of UTC shall be used.\r\nIgnored if begin was specified with " + + "offset and timezone."), @OpenApiParam(name = BEGIN, description = "The start time of the updates to " + "the time series. If NULL or not specified, the generated message " + "will not include this item."), diff --git a/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java b/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java index a428d913e6..50c58904f2 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/project/UpdateLockRevokerRights.java @@ -29,6 +29,7 @@ import static cwms.cda.api.Controllers.PROJECT_MASK; import static cwms.cda.api.Controllers.USER_ID; import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.requiredParamAs; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; @@ -82,8 +83,7 @@ public void handle(@NotNull Context ctx) throws Exception { String userId = requiredParam(ctx, USER_ID); String projMask = ctx.queryParamAsClass(PROJECT_MASK, String.class).getOrDefault("*"); String appId = requiredParam(ctx, APPLICATION_ID); - Boolean allow = ctx.queryParamAsClass(Controllers.ALLOW, Boolean.class) - .getOrThrow(e -> new RequiredQueryParameterException(Controllers.ALLOW)); + Boolean allow = requiredParamAs(ctx, Controllers.ALLOW, Boolean.class); try (final Timer.Context ignored = markAndTime("updateRights")) { ProjectLockDao lockDao = new ProjectLockDao(JooqDao.getDslContext(ctx)); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java index 691859fb93..3658162424 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingController.java @@ -52,18 +52,24 @@ import static cwms.cda.api.Controllers.UPDATE; import static cwms.cda.api.Controllers.VERSION_DATE; import static cwms.cda.api.Controllers.addDeprecatedContentTypeWarning; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.BaseCrudHandler; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.RatingDao; import cwms.cda.data.dao.RatingSetDao; +import cwms.cda.data.dao.RatingsVerticalDatumExtractor; +import cwms.cda.data.dao.VerticalDatum; import cwms.cda.data.dto.CwmsDTOBase; import cwms.cda.data.dto.StatusResponse; +import cwms.cda.data.dto.VerticalDatumInfo; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; @@ -88,30 +94,26 @@ import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import javax.xml.transform.TransformerException; + import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import mil.army.usace.hec.metadata.VerticalDatumException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jooq.DSLContext; -public class RatingController implements CrudHandler { +public class RatingController extends BaseCrudHandler { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); static final String TAG = "Ratings"; - private final MetricRegistry metrics; - - private final Histogram requestResultSize; - static { JavalinValidation.register(RatingSet.DatabaseLoadMethod.class, RatingController::getDatabaseLoadMethod); } public RatingController(MetricRegistry metrics) { - this.metrics = metrics; - String className = this.getClass().getName(); - requestResultSize = this.metrics.histogram((name(className, RESULTS, SIZE))); + super(metrics); } private static RatingSet.DatabaseLoadMethod getDatabaseLoadMethod(String input) { @@ -136,7 +138,16 @@ protected RatingDao getRatingDao(DSLContext dsl) { required = true), queryParams = { @OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class, - description = "Also store updates to the rating template. Default: true") + description = "Also store updates to the rating template. Default: true"), + @OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided " + + "rating-set includes an explicit vertical-datum-info attribute " + + "then it is assumed that the data is in the datum specified by the vertical-datum-info. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is not provided it is assumed that the data is in the as-stored " + + "datum and no conversion is necessary. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is provided it is assumed that the data is in the Datum named by the argument " + + "and should be converted to the as-stored datum before being saved.") }, method = HttpMethod.POST, path = "/ratings", tags = {TAG}, responses = { @@ -149,7 +160,14 @@ public void create(@NotNull Context ctx) { RatingDao ratingDao = getRatingDao(dsl); boolean storeTemplate = ctx.queryParamAsClass(STORE_TEMPLATE, Boolean.class).getOrDefault(true); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - ratingDao.create(ratingSet, false); + String datum = ctx.queryParam(DATUM); + VerticalDatum vd = null; + if(datum != null) { + vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + } + vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); + ratingDao.create(ratingSet, false, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Rating Set successfully stored to CWMS."); ctx.status(HttpServletResponse.SC_CREATED).json(re); } catch (IOException ex) { @@ -163,10 +181,6 @@ public void create(@NotNull Context ctx) { } } - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); - } - private String deserializeRatingSet(Context ctx, boolean storeTemplate) throws IOException, RatingException { String formatHeader = ctx.req.getContentType(); //Using placeholder CwmsDTOBase.class since we do not have a RatingSet DTO @@ -232,9 +246,9 @@ public void delete(@NotNull Context ctx, @NotNull String ratingSpecId) { DSLContext dsl = getDslContext(ctx); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); - Instant startTimeDate = DateUtils.parseUserDate(ctx.queryParam(BEGIN), timezone).toInstant(); - Instant endTimeDate = DateUtils.parseUserDate(ctx.queryParam(END), timezone).toInstant(); - String office = ctx.queryParam(OFFICE); + Instant startTimeDate = DateUtils.parseUserDate(requiredParam(ctx, BEGIN), timezone).toInstant(); + Instant endTimeDate = DateUtils.parseUserDate(requiredParam(ctx, END), timezone).toInstant(); + String office = requiredParam(ctx, OFFICE); RatingDao ratingDao = getRatingDao(dsl); ratingDao.delete(office, ratingSpecId, startTimeDate, endTimeDate); ctx.status(HttpServletResponse.SC_NO_CONTENT); @@ -265,7 +279,9 @@ public void delete(@NotNull Context ctx, @NotNull String ratingSpecId) { + "\n* `NAVD88` The elevation values will in the " + "specified or default units above the NAVD-88 datum." + "\n* `NGVD29` The elevation values will be in the " - + "specified or default units above the NGVD-29 datum."), + + "specified or default units above the NGVD-29 datum." + + "\n* `NATIVE` The elevation values will be in the " + + "Location's native datum."), @OpenApiParam(name = AT, description = "Specifies the " + "start of the time window for data to be included in the response. " + "If this field is not specified, any required time window begins 24" @@ -336,7 +352,7 @@ public void getAll(@NotNull Context ctx) { ctx.status(HttpServletResponse.SC_OK); ctx.result(results); addDeprecatedContentTypeWarning(ctx, contentType); - requestResultSize.update(results.length()); + updateResultSize(results.length()); } } @@ -362,6 +378,16 @@ public void getAll(@NotNull Context ctx) { @OpenApiParam(name = METHOD, description = "Specifies " + "the retrieval method used. If no method is provided EAGER will be used.", type = RatingSet.DatabaseLoadMethod.class), + @OpenApiParam(name = DATUM, description = "Specifies the " + + "elevation datum of the response. This field affects only elevation" + + " Ratings. Valid values for this field are:" + + "\n* `NAVD88` The elevation values will in the " + + "specified or default units above the NAVD-88 datum." + + "\n* `NGVD29` The elevation values will be in the " + + "specified or default units above the NGVD-29 datum." + + "\n* `NATIVE` The elevation values will be in the " + + "Location's native datum.", + type = VerticalDatum.class), }, responses = { @OpenApiResponse(status = STATUS_200, content = { @@ -375,8 +401,9 @@ public void getAll(@NotNull Context ctx) { public void getOne(@NotNull Context ctx, @NotNull String rating) { try (final Timer.Context ignored = markAndTime(GET_ONE)) { - String officeId = ctx.queryParam(OFFICE); + String officeId = requiredParam(ctx, OFFICE); String timezone = ctx.queryParamAsClass(TIMEZONE, String.class).getOrDefault("UTC"); + VerticalDatum verticalDatum = VerticalDatum.getVerticalDatum(ctx.queryParam(DATUM)); Instant beginInstant = null; String begin = ctx.queryParam(BEGIN); @@ -394,7 +421,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { RatingSet.DatabaseLoadMethod.class) .getOrDefault(RatingSet.DatabaseLoadMethod.EAGER); - String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant); + String body = getRatingSetString(ctx, method, officeId, rating, beginInstant, endInstant, verticalDatum); if (body != null) { ctx.result(body); ctx.status(HttpCode.OK); @@ -406,7 +433,7 @@ public void getOne(@NotNull Context ctx, @NotNull String rating) { @Nullable private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod method, String officeId, String rating, Instant begin, - Instant end) { + Instant end, VerticalDatum verticalDatum) { String retval = null; try (final Timer.Context ignored = markAndTime("getRatingSetString")) { @@ -421,9 +448,44 @@ private String getRatingSetString(Context ctx, RatingSet.DatabaseLoadMethod meth try { RatingSet ratingSet = getRatingSet(ctx, method, officeId, rating, begin, end); if (ratingSet != null) { + //Apply vertical datum conversion if needed + if (verticalDatum != null) { + try { + switch (verticalDatum) + { + case NAVD88: + ratingSet.toNAVD88(); + break; + case NGVD29: + ratingSet.toNGVD29(); + break; + case NATIVE: + ratingSet.toNativeVerticalDatum(); + break; + default: + logger.atSevere().log("Unknown vertical datum: %s", verticalDatum); + break; + } + VerticalDatumInfo vdi = RatingsVerticalDatumExtractor.deserializeVerticalDatumInfoXml(ratingSet.getVerticalDatumInfo()); + if(vdi != null && vdi.getOffsetForDatum(verticalDatum) != null) { + VerticalDatumInfo newVdi = vdi.convertedTo(vdi.getOffsetForDatum(verticalDatum)); + XmlMapper xmlMapper = new XmlMapper(); + String vdiXml = xmlMapper.writeValueAsString(newVdi); + ratingSet.setVerticalDatumInfo(vdiXml); + } + } catch (VerticalDatumException vde) { + logger.atWarning().withCause(vde).log("Failed to convert rating %s to requested vertical datum: %s", + rating, verticalDatum); + } + } if (isJson) { retval = JsonRatingUtils.toJson(ratingSet); } else { + //the toXml method in RatingXmlFactory converts to native-datum which breaks things coming back in the user-requested datum + //setting the current-datum to an unknown value prevents the call to convert to native-datum + if(ratingSet.getVerticalDatumContainer() != null && ratingSet.getVerticalDatumContainer().currentDatum != null) { + ratingSet.getVerticalDatumContainer().currentDatum = "ignoreConversionToNativeDatum"; + } retval = RatingXmlFactory.toXml(ratingSet, " "); } } else { @@ -483,11 +545,20 @@ private RatingSet getRatingSet(Context ctx, RatingSet.DatabaseLoadMethod method, @OpenApiParam(name = STORE_TEMPLATE, type = Boolean.class, description = "Also store updates to the rating template. Default: true"), @OpenApiParam(name = REPLACE_BASE_CURVE, type = Boolean.class, - description = "Replace the base curve of USGS stream flow rating. Default: false") + description = "Replace the base curve of USGS stream flow rating. Default: false"), + @OpenApiParam(name = DATUM, type = VerticalDatum.class, description = "If the provided " + + "rating-set includes an explicit vertical-datum-info attribute " + + "then it is assumed that the data is in the datum specified by the vertical-datum-info. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is not provided it is assumed that the data is in the as-stored " + + "datum and no conversion is necessary. " + + "If the input rating-set does not include vertical-datum-info and " + + "this parameter is provided it is assumed that the data is in the Datum named by the argument " + + "and should be converted to the as-stored datum before being saved.") }, method = HttpMethod.PATCH, path = "/ratings", tags = {TAG}) public void update(@NotNull Context ctx, @NotNull String ratingId) { - + logUnusedPathParameter(ctx, RATING_ID, "Body contains required information"); try (final Timer.Context ignored = markAndTime(UPDATE)) { DSLContext dsl = getDslContext(ctx); @@ -498,7 +569,14 @@ public void update(@NotNull Context ctx, @NotNull String ratingId) { boolean replaceBaseCurve = ctx.queryParamAsClass(REPLACE_BASE_CURVE, Boolean.class) .getOrDefault(false); String ratingSet = deserializeRatingSet(ctx, storeTemplate); - ratingDao.store(ratingSet, replaceBaseCurve); + String datum = ctx.queryParam(DATUM); + VerticalDatum vd = null; + if(datum != null) { + vd = ctx.queryParamAsClass(DATUM, VerticalDatum.class) + .getOrDefault(null); + } + vd = RatingsVerticalDatumExtractor.getVerticalDatum(ratingSet).orElse(vd); + ratingDao.store(ratingSet, replaceBaseCurve, vd); StatusResponse re = new StatusResponse(RatingDao.extractOfficeFromXml(ratingSet), "Updated RatingSet"); ctx.status(HttpServletResponse.SC_OK).json(re); } catch (IOException ex) { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java index c7db67d170..aff922c105 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingLatestController.java @@ -28,6 +28,7 @@ import static cwms.cda.api.Controllers.OFFICE; import static cwms.cda.api.Controllers.RATING_ID; import static cwms.cda.api.Controllers.STATUS_200; +import static cwms.cda.api.Controllers.requiredParam; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.MetricRegistry; @@ -91,7 +92,7 @@ public void handle(@NotNull Context ctx) throws Exception { ContentType contentType = new ContentType(ctx.contentType() != null ? ctx.contentType() : Formats.JSONV2); - String officeId = ctx.queryParam(OFFICE); + String officeId = requiredParam(ctx, OFFICE); if (!contentType.toString().equals(Formats.JSONV2) && !contentType.toString().equals(Formats.XMLV2)) { ctx.status(HttpCode.UNSUPPORTED_MEDIA_TYPE); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingMetadataController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingMetadataController.java index eba0d857a4..ce95a2f178 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingMetadataController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingMetadataController.java @@ -179,8 +179,7 @@ public void getAll(Context ctx) { @OpenApi(ignore = true) @Override public void getOne(Context ctx, String ratingId) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @NotNull @@ -192,22 +191,19 @@ protected RatingMetadataDao getDao(DSLContext dsl) { @OpenApi(ignore = true) @Override public void create(Context ctx) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void update(Context ctx, String locationCode) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi(ignore = true) @Override public void delete(Context ctx, String locationCode) { - throw new UnsupportedOperationException(NOT_SUPPORTED_YET); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java index 4b5477a6f2..63f157b450 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingSpecController.java @@ -33,12 +33,12 @@ import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.JooqDao; -import cwms.cda.data.dao.JsonRatingUtils; import cwms.cda.data.dao.RatingSpecDao; import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.data.dto.rating.RatingSpecs; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.formatters.FormattingException; import io.javalin.apibuilder.CrudHandler; import io.javalin.core.util.Header; import io.javalin.http.Context; @@ -48,11 +48,13 @@ import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; -import java.io.IOException; + import java.util.Optional; + import com.google.common.flogger.FluentLogger; + import javax.servlet.http.HttpServletResponse; -import javax.xml.transform.TransformerException; + import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -63,7 +65,7 @@ public class RatingSpecController implements CrudHandler { private final MetricRegistry metrics; - private static final int DEFAULT_PAGE_SIZE = 100; + static final int DEFAULT_PAGE_SIZE = 100; private final Histogram requestResultSize; @@ -100,13 +102,14 @@ private Timer.Context markAndTime(String subject) { ), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "How many entries per page returned. " - + "Default " + DEFAULT_PAGE_SIZE + "." + + "Default " + DEFAULT_PAGE_SIZE + "." ), }, responses = { @OpenApiResponse(status = STATUS_200, content = { - @OpenApiContent(type = Formats.JSONV2, from = RatingSpecs.class) + @OpenApiContent(type = Formats.JSONV2, from = RatingSpecs.class), + @OpenApiContent(type = Formats.XMLV2, from = RatingSpecs.class) } )}, tags = {TAG} @@ -122,7 +125,7 @@ public void getAll(Context ctx) { String formatHeader = ctx.header(Header.ACCEPT); ContentType contentType = Formats.parseHeader(formatHeader, RatingSpecs.class); - try (final Timer.Context timeContext = markAndTime(GET_ALL)){ + try (final Timer.Context ignored = markAndTime(GET_ALL)) { DSLContext dsl = getDslContext(ctx); RatingSpecDao ratingSpecDao = getRatingSpecDao(dsl); @@ -150,7 +153,7 @@ public void getAll(Context ctx) { + "the rating-id of the Rating Spec to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the Rating Specs whose data is to be included in " + "the response. If this field is not specified, matching rating " + "information from all offices shall be returned."), @@ -159,6 +162,7 @@ public void getAll(Context ctx) { @OpenApiResponse(status = STATUS_200, content = { @OpenApiContent(from = RatingSpec.class, type = Formats.JSONV2), + @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) } ) }, @@ -171,7 +175,7 @@ public void getOne(Context ctx, String ratingId) { String office = ctx.queryParam(OFFICE); - try (final Timer.Context timeContext = markAndTime(GET_ONE)){ + try (final Timer.Context ignored = markAndTime(GET_ONE)) { DSLContext dsl = getDslContext(ctx); RatingSpecDao ratingSpecDao = getRatingSpecDao(dsl); @@ -201,88 +205,79 @@ protected RatingSpecDao getRatingSpecDao(DSLContext dsl) { @OpenApi( - description = "Create new Rating Specification", - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) + description = "Create new Rating Specification", + requestBody = @OpenApiRequestBody( + content = { + @OpenApiContent(from = RatingSpec.class, type = Formats.JSON), + @OpenApiContent(from = RatingSpec.class, type = Formats.XMLV2) + }, + required = true), + queryParams = { + @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, + description = "Create will fail if provided ID already exists. Default: true") }, - required = true), - queryParams = { - @OpenApiParam(name = FAIL_IF_EXISTS, type = Boolean.class, - description = "Create will fail if provided ID already exists. Default: true") - }, - method = HttpMethod.POST, - tags = {TAG} + method = HttpMethod.POST, + tags = {TAG} ) @Override public void create(Context ctx) { - try (final Timer.Context ignored = markAndTime(CREATE)){ + try (final Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); - String reqContentType = ctx.req.getContentType(); - String formatHeader = reqContentType != null ? reqContentType : Formats.XMLV2; + String contentTypeHeader = ctx.req.getContentType(); String body = ctx.body(); - String xml = translateToXml(body, formatHeader); - RatingSpecDao dao = new RatingSpecDao(dsl); - boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); - dao.create(xml, failIfExists); - ctx.status(HttpServletResponse.SC_CREATED); - } - } + ContentType contentType = Formats.parseHeader(contentTypeHeader, RatingSpec.class); - private static String translateToXml(String body, String contentType) { - String retval; + boolean failIfExists = ctx.queryParamAsClass(FAIL_IF_EXISTS, Boolean.class).getOrDefault(false); + RatingSpecDao dao = new RatingSpecDao(dsl); - if (contentType.contains(Formats.XMLV2)) { - retval = body; - } else if (contentType.contains(Formats.JSONV2)) { - retval = translateJsonToXml(body); - } else { - throw new IllegalArgumentException("Unexpected contentType format:" + contentType); + try { + RatingSpec spec = Formats.parseContent(contentType, body, RatingSpec.class); + // If we can parse it into our CDA RatingSpec object have the DAO use it. + dao.create(spec, failIfExists); + ctx.status(HttpServletResponse.SC_CREATED); + } catch (FormattingException e) { + if (contentType.getType().contains(Formats.XML)) { + // The user said its xml but it doesn't parse into our CDA RatingSpec object. + // We'll let the dao try doing a string pass-thru to the pl/sql. + dao.create(body, failIfExists); + ctx.status(HttpServletResponse.SC_CREATED); + return; + } + throw e; + } } - - return retval; } - private static String translateJsonToXml(String body) { - String retval; - try { - retval = JsonRatingUtils.jsonToXml(body); - } catch (IOException | TransformerException ex) { - throw new IllegalArgumentException("Failed to translate request into rating spec XML", ex); - } - return retval; - } @OpenApi(ignore = true) @Override public void update(Context ctx, String locationCode) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of - // generated methods, choose Tools | Specs. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( - pathParams = { - @OpenApiParam(name = RATING_ID, required = true, description = "The rating-spec-id of the ratings data to be deleted."), - }, - queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " - + "owning office of the ratings to be deleted."), - @OpenApiParam(name = METHOD, required = true, description = "Specifies the delete method used.", - type = JooqDao.DeleteMethod.class) - }, - description = "Deletes requested rating specification", - method = HttpMethod.DELETE, - tags = {TAG} + pathParams = { + @OpenApiParam(name = RATING_ID, required = true, description = "The rating-spec-id of the ratings data to be deleted."), + }, + queryParams = { + @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + + "owning office of the ratings to be deleted."), + @OpenApiParam(name = METHOD, required = true, description = "Specifies the delete method used.", + type = JooqDao.DeleteMethod.class) + }, + description = "Deletes requested rating specification", + method = HttpMethod.DELETE, + tags = {TAG} ) @Override public void delete(Context ctx, @NotNull String ratingSpecId) { - try (final Timer.Context ignored = markAndTime(DELETE)){ + try (final Timer.Context ignored = markAndTime(DELETE)) { DSLContext dsl = getDslContext(ctx); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); RatingSpecDao ratingDao = getRatingSpecDao(dsl); - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); ratingDao.delete(office, method, ratingSpecId); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java index d0cf75b7e0..a56b6ed50c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rating/RatingTemplateController.java @@ -154,7 +154,7 @@ private RatingTemplateDao getRatingTemplateDao(DSLContext dsl) { + " the template whose data is to be included in the response") }, queryParams = { - @OpenApiParam(name = OFFICE, required = true, description = "Specifies the " + @OpenApiParam(name = OFFICE, description = "Specifies the " + "owning office of the Rating Templates whose data is to be included" + " in the response. If this field is not specified, matching rating " + "information from all offices shall be returned."), @@ -234,7 +234,7 @@ private static String translateToXml(String body, String contentType) { String retval; - if (contentType.contains(Formats.XMLV2)) { + if (contentType.contains(Formats.XMLV2) || contentType.contains(Formats.XML)) { retval = body; } else if (contentType.contains(Formats.JSONV2)) { retval = translateJsonToXml(body); @@ -258,8 +258,7 @@ private static String translateJsonToXml(String body) { @OpenApi(ignore = true) @Override public void update(Context ctx, String locationCode) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of - // generated methods, choose Tools | Templates. + ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented()); } @OpenApi( @@ -281,9 +280,9 @@ public void delete(Context ctx, String ratingTemplateId) { try (final Timer.Context ignored = markAndTime(DELETE)){ DSLContext dsl = getDslContext(ctx); - String office = ctx.queryParam(OFFICE); + String office = requiredParam(ctx, OFFICE); RatingTemplateDao ratingDao = new RatingTemplateDao(dsl); - JooqDao.DeleteMethod method = ctx.queryParamAsClass(METHOD, JooqDao.DeleteMethod.class).get(); + JooqDao.DeleteMethod method = requiredParamAs(ctx, METHOD, JooqDao.DeleteMethod.class); ratingDao.delete(office, method, ratingTemplateId); ctx.status(HttpServletResponse.SC_NO_CONTENT); } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/rss/RssHandler.java b/cwms-data-api/src/main/java/cwms/cda/api/rss/RssHandler.java index 43d5b8f65e..c264599251 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/rss/RssHandler.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/rss/RssHandler.java @@ -24,23 +24,13 @@ package cwms.cda.api.rss; -import static cwms.cda.api.Controllers.CURSOR; -import static cwms.cda.api.Controllers.GET_ALL; -import static cwms.cda.api.Controllers.NAME; -import static cwms.cda.api.Controllers.OFFICE; -import static cwms.cda.api.Controllers.PAGE; -import static cwms.cda.api.Controllers.PAGE_SIZE; -import static cwms.cda.api.Controllers.SINCE; -import static cwms.cda.api.Controllers.STATUS_200; -import static cwms.cda.api.Controllers.STATUS_400; -import static cwms.cda.api.Controllers.STATUS_404; -import static cwms.cda.api.Controllers.queryParamAsClass; -import static cwms.cda.api.Controllers.queryParamAsInstant; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import cwms.cda.api.BaseHandler; +import cwms.cda.api.enums.MessageQueue; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.rss.MessageDao; import cwms.cda.data.dto.CwmsDTOPaginated; @@ -77,17 +67,29 @@ public RssHandler(MetricRegistry metrics) { @OpenApi( pathParams = { @OpenApiParam(name = OFFICE, required = true, description = "Office id for feed."), - @OpenApiParam(name = NAME, required = true, description = "Specifies the name of the feed. " + + @OpenApiParam(name = NAME, required = true, + type = MessageQueue.class, + description = "Specifies the name of the feed. " + "eg TS_STORED, STATUS, REALTIME_OPS") }, queryParams = { + @OpenApiParam(name = TIMEZONE, description = "Specifies " + + "the time zone of the values of " + SINCE + " fields (unless " + + "otherwise specified). If this field is not specified, the default time zone " + + "of UTC shall be used.\r\nIgnored if " + SINCE + " was specified with " + + "offset and timezone."), @OpenApiParam(name = SINCE, description = "The start the feed time window. " + "The endpoint will not retrieve more than the last week of messages."), @OpenApiParam(name = PAGE_SIZE, type = Integer.class, description = "The number of feed items to include."), @OpenApiParam(name = PAGE, description = "This end point can return a lot of data, this " + "identifies where in the request you are. This is an opaque" + " value, and can be obtained from the 'next-page' value in " - + "the response.") + + "the response."), + @OpenApiParam(name = CURSOR, deprecated = true, + description = "This end point can return a lot of data, this " + + "identifies where in the request you are. This is an opaque" + + " value, and can be obtained from the 'next-page' value in " + + "the response. Deprecated, use " + PAGE + " instead."), }, responses = { @OpenApiResponse(status = STATUS_200, content = { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java index 38c2fdfab0..6e535b9ebb 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileController.java @@ -60,7 +60,8 @@ public TimeSeriesProfileController(MetricRegistry metrics) { @OpenApi( queryParams = { - @OpenApiParam(name = OFFICE, description = "The office ID associated with the time series profile"), + @OpenApiParam(name = OFFICE, required = true, + description = "The office ID associated with the time series profile"), }, pathParams = { @OpenApiParam(name = PARAMETER_ID, description = "The key parameter ID associated with the time " diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java index 4a15335b25..04d8a4803b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileDeleteController.java @@ -59,7 +59,8 @@ public TimeSeriesProfileDeleteController(MetricRegistry metrics) { @OpenApi( queryParams = { - @OpenApiParam(name = OFFICE, description = "The office associated with the time series profile"), + @OpenApiParam(name = OFFICE, required = true, + description = "The office associated with the time series profile"), }, pathParams = { @OpenApiParam(name = LOCATION_ID, description = "The location ID associated with the time " diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceController.java index ceca847df6..f8cb337955 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceController.java @@ -95,10 +95,12 @@ public TimeSeriesProfileInstanceController(MetricRegistry metrics) { + " time series profile instance. Default is true"), @OpenApiParam(name = END_TIME_INCLUSIVE, type = Boolean.class, description = "The end inclusive of the" + " time series profile instance. Default is true"), - @OpenApiParam(name = PREVIOUS, type = boolean.class, description = "Whether to include the previous " - + " time window of the time series profile instance. Default is false"), - @OpenApiParam(name = NEXT, type = boolean.class, description = "Whether to include the next time window " - + "of the time series profile instance. Default is false"), + @OpenApiParam(name = PREVIOUS, type = boolean.class, description = "Whether to include the data point " + + "with the closest timestamp prior to the specified start of the time window for the time series " + + "profile instance. Default is false"), + @OpenApiParam(name = NEXT, type = boolean.class, description = "Whether to include the data point with " + + "the closest timestamp after the specified end of the time window for the time series profile " + + "instance. Default is false"), @OpenApiParam(name = MAX_VERSION, type = boolean.class, description = "Whether to use the max version" + " date of the time series profile instance. Default is false. If no version date is provided, and" + " maxVersion is false, the min version date will be used."), diff --git a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java index b85da41c04..2f7682a34c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/timeseriesprofile/TimeSeriesProfileInstanceCreateController.java @@ -26,14 +26,7 @@ package cwms.cda.api.timeseriesprofile; -import static cwms.cda.api.Controllers.CREATE; -import static cwms.cda.api.Controllers.METHOD; -import static cwms.cda.api.Controllers.OVERRIDE_PROTECTION; -import static cwms.cda.api.Controllers.PROFILE_DATA; -import static cwms.cda.api.Controllers.VERSION; -import static cwms.cda.api.Controllers.VERSION_DATE; -import static cwms.cda.api.Controllers.requiredInstant; -import static cwms.cda.api.Controllers.requiredParam; +import static cwms.cda.api.Controllers.*; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.MetricRegistry; @@ -42,6 +35,7 @@ import cwms.cda.data.dao.timeseriesprofile.TimeSeriesProfileInstanceDao; import cwms.cda.data.dto.timeseriesprofile.TimeSeriesProfile; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.http.Context; import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.HttpMethod; @@ -67,6 +61,11 @@ public TimeSeriesProfileInstanceCreateController(MetricRegistry metrics) { + " time series profile instance. Default is REPLACE_ALL"), @OpenApiParam(name = OVERRIDE_PROTECTION, type = Boolean.class, description = "Override protection" + " for the time series profile instance. Default is false"), + @OpenApiParam(name = TIMEZONE, description = "Specifies " + + "the time zone of the values of " + VERSION_DATE + " fields (unless " + + "otherwise specified). If this field is not specified, the default time zone " + + "of UTC shall be used.\r\nIgnored if " + VERSION_DATE + " was specified with " + + "offset and timezone."), @OpenApiParam(name = VERSION_DATE, type = Instant.class, description = "The version date of the" + " time series profile instance. Accepts ISO8601 format.", required = true), @OpenApiParam(name = PROFILE_DATA, required = true, description = "The profile data of the" @@ -84,6 +83,7 @@ public TimeSeriesProfileInstanceCreateController(MetricRegistry metrics) { @OpenApiResponse(status = "409", description = "Time series profile instance already exists") } ) + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(@NotNull Context ctx) { try (final Timer.Context ignored = markAndTime(CREATE)) { diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java index 1e10310c51..3393d3d92c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCatalogController.java @@ -26,6 +26,7 @@ package cwms.cda.api.watersupply; +import static cwms.cda.api.Controllers.ACCEPT; import static cwms.cda.api.Controllers.BEGIN; import static cwms.cda.api.Controllers.CONTRACT_NAME; import static cwms.cda.api.Controllers.END; @@ -42,11 +43,13 @@ import static cwms.cda.api.Controllers.TIME_FORMAT_DESC; import static cwms.cda.api.Controllers.UNIT; import static cwms.cda.api.Controllers.WATER_USER; +import static cwms.cda.api.Controllers.queryParamAsClass; import static cwms.cda.api.Controllers.requiredInstant; import static cwms.cda.data.dao.JooqDao.getDslContext; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.Controllers; import cwms.cda.api.errors.CdaError; import cwms.cda.data.dao.watersupply.WaterContractDao; @@ -57,6 +60,7 @@ import cwms.cda.data.dto.watersupply.WaterUserContract; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.core.util.Header; import io.javalin.http.Context; import io.javalin.http.Handler; @@ -67,8 +71,8 @@ import io.javalin.plugin.openapi.annotations.OpenApiResponse; import java.time.Instant; import java.util.List; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; @@ -136,32 +140,40 @@ protected WaterSupplyAccountingDao getWaterSupplyAccountingDao(DSLContext dsl) { + "provided input parameters."), @OpenApiResponse(status = STATUS_501, description = "Requested format is not implemented") }, + headers = { + @OpenApiParam(name = ACCEPT, description = "The requested response format. Supported values are " + + Formats.JSONV1 + ", " + Formats.JSON + ", and " + Formats.JSONV2 + ". " + Formats.JSONV2 + + " should be used only when providing a URL-safe base64 encoded contract ID. If not provided, " + + Formats.JSONV1 + " will be used as the default.") + }, description = "Get pump accounting entries associated with a water supply contract.", path = "/projects/{office}/water-user/{water-user}/contracts/{contract-name}/accounting", method = HttpMethod.GET, tags = {TAG} ) - + @IgnoreRequiredQueryParamMismatch(parameterNames = {TIMEZONE}) @Override public void handle(Context ctx) { try (Timer.Context ignored = markAndTime(GET_ALL)) { final String office = ctx.pathParam(OFFICE); final String waterUserName = ctx.pathParam(WATER_USER); - final String contractId = ctx.pathParam(CONTRACT_NAME); + String contractId = ctx.pathParam(CONTRACT_NAME); final String locationId = ctx.pathParam(PROJECT_ID); final Instant startTime = requiredInstant(ctx, START); final Instant endTime = requiredInstant(ctx, END); - final String units = ctx.queryParam(UNIT) != null ? ctx.queryParam(UNIT) : "cms"; - final boolean startInclusive = ctx.queryParam(START_TIME_INCLUSIVE) == null - || Boolean.parseBoolean(ctx.queryParam(START_TIME_INCLUSIVE)); - final boolean endInclusive = ctx.queryParam(END_TIME_INCLUSIVE) == null - || Boolean.parseBoolean(ctx.queryParam(END_TIME_INCLUSIVE)); - final boolean ascending = ctx.queryParam(ASCENDING) == null - || Boolean.parseBoolean(ctx.queryParam(ASCENDING)); - final int rowLimit = ctx.queryParam(ROW_LIMIT) != null ? Integer.parseInt(ctx.queryParam(ROW_LIMIT)) : 0; + final String units = ctx.queryParamAsClass(UNIT, String.class).getOrDefault("cms"); + final boolean startInclusive = ctx.queryParamAsClass(START_TIME_INCLUSIVE, Boolean.class) + .getOrDefault(true); + final boolean endInclusive = ctx.queryParamAsClass(END_TIME_INCLUSIVE, Boolean.class).getOrDefault(true); + final boolean ascending = ctx.queryParamAsClass(ASCENDING, Boolean.class).getOrDefault(true); + final int rowLimit = ctx.queryParamAsClass(ROW_LIMIT, Integer.class).getOrDefault(0); DSLContext dsl = getDslContext(ctx); - String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : Formats.JSONV1; + String formatHeader = ctx.headerAsClass(Header.ACCEPT, String.class).getOrDefault(Formats.JSONV1); + if (formatHeader != null && formatHeader.equals(Formats.JSONV2)) { + byte[] decoded = Base64.decodeBase64(contractId); + contractId = new String(decoded); + } ContentType contentType = Formats.parseHeader(formatHeader, WaterSupplyAccounting.class); ctx.contentType(contentType.toString()); CwmsId projectLocation = new CwmsId.Builder().withOfficeId(office).withName(locationId).build(); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCreateController.java index b2779fffe3..a381517fa0 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/AccountingCreateController.java @@ -26,6 +26,7 @@ package cwms.cda.api.watersupply; +import static cwms.cda.api.Controllers.ACCEPT; import static cwms.cda.api.Controllers.CONTRACT_NAME; import static cwms.cda.api.Controllers.CREATE; import static cwms.cda.api.Controllers.OFFICE; @@ -36,9 +37,10 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; -import cwms.cda.api.Controllers; +import cwms.cda.api.BaseHandler; import cwms.cda.data.dao.LookupTypeDao; import cwms.cda.data.dao.watersupply.WaterSupplyAccountingDao; +import cwms.cda.data.dao.watersupply.WaterSupplyUtils; import cwms.cda.data.dto.LookupType; import cwms.cda.data.dto.StatusResponse; import cwms.cda.data.dto.watersupply.PumpTransfer; @@ -47,31 +49,29 @@ import cwms.cda.formatters.Formats; import io.javalin.core.util.Header; import io.javalin.http.Context; -import io.javalin.http.Handler; import io.javalin.plugin.openapi.annotations.HttpMethod; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiContent; import io.javalin.plugin.openapi.annotations.OpenApiParam; import io.javalin.plugin.openapi.annotations.OpenApiRequestBody; import io.javalin.plugin.openapi.annotations.OpenApiResponse; +import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.Arrays; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.metadata.DataSetIllegalArgumentException; +import org.apache.commons.codec.binary.Base64; import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; -public class AccountingCreateController implements Handler { +public class AccountingCreateController extends BaseHandler { private static final String TAG = "Pump Accounting"; - private final MetricRegistry metrics; - - private Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); - } public AccountingCreateController(MetricRegistry metrics) { - this.metrics = metrics; + super(metrics); } @NotNull @@ -91,12 +91,18 @@ protected WaterSupplyAccountingDao getWaterSupplyAccountingDao(DSLContext dsl) { @OpenApiParam(name = WATER_USER, description = "The water user the accounting is associated with.", required = true), @OpenApiParam(name = CONTRACT_NAME, description = "The name of the contract associated with the " - + "accounting.", required = true), + + "accounting. For names with special characters (such as '/'), use the JSONV2 accept header " + + "with a name encoded using URL-safe BASE64.", required = true), }, responses = { @OpenApiResponse(status = STATUS_201, description = "The pump accounting entry was created."), @OpenApiResponse(status = STATUS_501, description = "Requested format is not implemented") }, + headers = { + @OpenApiParam(name = ACCEPT, description = "The format of the request body. Accepts JSONV1 and JSONV2. " + + "Note: JSONV2 should be used if the contract name contains special characters such as '/'. " + + "In this case, the contract name should be encoded using URL-safe BASE64 encoding.") + }, description = "Create a new pump accounting entry associated with a water supply contract.", path = "/projects/{office}/water-user/{water-user}/contracts/{contract-name}/accounting", method = HttpMethod.POST, @@ -105,11 +111,20 @@ protected WaterSupplyAccountingDao getWaterSupplyAccountingDao(DSLContext dsl) { @Override public void handle(@NotNull Context ctx) { + logUnusedPathParameter(ctx, WATER_USER, "Body contains required information."); + try (Timer.Context ignored = markAndTime(CREATE)) { - final String contractId = ctx.pathParam(CONTRACT_NAME); + String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : Formats.JSONV1; + String contractId; + if (formatHeader != null && formatHeader.equals(Formats.JSONV2)) { + byte[] decoded = Base64.decodeBase64(ctx.pathParam(CONTRACT_NAME)); + contractId = new String(decoded); + } else { + contractId = ctx.pathParam(CONTRACT_NAME); + } + final String office = ctx.pathParam(OFFICE); DSLContext dsl = getDslContext(ctx); - String formatHeader = ctx.header(Header.ACCEPT) != null ? ctx.header(Header.ACCEPT) : Formats.JSONV1; ContentType contentType = Formats.parseHeader(formatHeader, WaterSupplyAccounting.class); ctx.contentType(contentType.toString()); WaterSupplyAccounting accounting = Formats.parseContent(contentType, ctx.body(), @@ -132,9 +147,15 @@ public void handle(@NotNull Context ctx) { } } - waterSupplyAccountingDao.storeAccounting(accounting); - StatusResponse re = new StatusResponse(office, "The pump accounting entry was created.", contractId); - ctx.status(HttpServletResponse.SC_CREATED).json(re); + // Ensure flows are stored in SI units + try { + WaterSupplyAccounting accountingInSi = WaterSupplyUtils.convertAccountingFlowsToSi(accounting); + waterSupplyAccountingDao.storeAccounting(accountingInSi); + StatusResponse re = new StatusResponse(office, "The pump accounting entry was created.", contractId); + ctx.status(HttpServletResponse.SC_CREATED).json(re); + } catch (DataSetIllegalArgumentException | IllegalArgumentException ex) { + ctx.status(HttpServletResponse.SC_BAD_REQUEST).json("Unable to process units for flow: " + ex.getMessage()); + } } } @@ -146,4 +167,6 @@ private boolean searchForTransferType(PumpTransfer accounting, List } return false; } + + } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCatalogController.java index 8e31f1dbfe..26a1d0e68c 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCatalogController.java @@ -54,10 +54,10 @@ import org.jooq.DSLContext; -public final class WaterContractCatalogController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractCatalogController extends WaterSupplyControllerBase { public WaterContractCatalogController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java index 9b208dc285..43263274a1 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractController.java @@ -56,10 +56,10 @@ import org.jooq.DSLContext; -public final class WaterContractController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractController extends WaterSupplyControllerBase { public WaterContractController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCreateController.java index 1748d2ed65..304511f300 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractCreateController.java @@ -56,10 +56,10 @@ import org.jooq.DSLContext; -public final class WaterContractCreateController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractCreateController extends WaterSupplyControllerBase { public WaterContractCreateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( @@ -94,6 +94,8 @@ public WaterContractCreateController(MetricRegistry metrics) { @Override public void handle(@NotNull Context ctx) { + logUnusedPathParameter(ctx, PROJECT_ID, "Body contains required information."); + logUnusedPathParameter(ctx, OFFICE, "Body contains required information."); try (Timer.Context ignored = markAndTime(CREATE)) { DSLContext dsl = getDslContext(ctx); String formatHeader = ctx.req.getContentType(); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractDeleteController.java index 409a39aa08..0b0b8304c3 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractDeleteController.java @@ -52,9 +52,9 @@ import org.jooq.DSLContext; -public final class WaterContractDeleteController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractDeleteController extends WaterSupplyControllerBase { public WaterContractDeleteController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java index 7a2ffa4e1e..ec92d906c8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCatalogController.java @@ -52,10 +52,10 @@ import org.jooq.DSLContext; -public final class WaterContractTypeCatalogController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractTypeCatalogController extends WaterSupplyControllerBase { public WaterContractTypeCatalogController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCreateController.java index 3e8bc44df3..003375014d 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeCreateController.java @@ -50,10 +50,10 @@ import org.jooq.DSLContext; -public final class WaterContractTypeCreateController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractTypeCreateController extends WaterSupplyControllerBase { public WaterContractTypeCreateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeDeleteController.java index 7a7849655d..1ccf26aa4e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractTypeDeleteController.java @@ -47,12 +47,12 @@ import org.jooq.DSLContext; -public final class WaterContractTypeDeleteController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractTypeDeleteController extends WaterSupplyControllerBase { private static final String DISPLAY_VALUE = "display-value"; public WaterContractTypeDeleteController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractUpdateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractUpdateController.java index ca6e6140d0..d7faded496 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractUpdateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterContractUpdateController.java @@ -56,10 +56,10 @@ import org.jooq.DSLContext; -public final class WaterContractUpdateController extends WaterSupplyControllerBase implements Handler { +public final class WaterContractUpdateController extends WaterSupplyControllerBase { public WaterContractUpdateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( @@ -96,6 +96,10 @@ public WaterContractUpdateController(MetricRegistry metrics) { @Override public void handle(@NotNull Context ctx) { + logUnusedPathParameter(ctx, PROJECT_ID, "Body contains required information."); + logUnusedPathParameter(ctx, OFFICE, "Body contains required information."); + logUnusedPathParameter(ctx, WATER_USER, "Body contains required information."); + try (Timer.Context ignored = markAndTime(UPDATE)) { DSLContext dsl = getDslContext(ctx); String contractName = ctx.pathParam(CONTRACT_NAME); diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterPumpDisassociateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterPumpDisassociateController.java index 5c3de2922a..1c1dffc2ff 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterPumpDisassociateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterPumpDisassociateController.java @@ -52,12 +52,12 @@ import org.jooq.DSLContext; -public final class WaterPumpDisassociateController extends WaterSupplyControllerBase implements Handler { +public final class WaterPumpDisassociateController extends WaterSupplyControllerBase { private static final String PUMP_TYPE = "pump-type"; private static final String DELETE_ACCOUNTING = "delete-accounting"; public WaterPumpDisassociateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterSupplyControllerBase.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterSupplyControllerBase.java index 7ff2d8aadd..1e521d4a41 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterSupplyControllerBase.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterSupplyControllerBase.java @@ -28,23 +28,19 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; +import cwms.cda.api.BaseHandler; import cwms.cda.api.Controllers; import cwms.cda.data.dao.watersupply.WaterContractDao; import org.jooq.DSLContext; -public class WaterSupplyControllerBase { +public abstract class WaterSupplyControllerBase extends BaseHandler { static final String TAG = "Water Contracts"; - private MetricRegistry metrics; - WaterContractDao getContractDao(DSLContext dsl) { - return new WaterContractDao(dsl); - } - - Timer.Context markAndTime(String subject) { - return Controllers.markAndTime(metrics, getClass().getName(), subject); + public WaterSupplyControllerBase(MetricRegistry metrics) { + super(metrics); } - void waterMetrics(MetricRegistry metrics) { - this.metrics = metrics; + WaterContractDao getContractDao(DSLContext dsl) { + return new WaterContractDao(dsl); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java index 758ca2f00a..253564b118 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCatalogController.java @@ -56,10 +56,10 @@ import org.jooq.DSLContext; -public final class WaterUserCatalogController extends WaterSupplyControllerBase implements Handler { +public final class WaterUserCatalogController extends WaterSupplyControllerBase { public WaterUserCatalogController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java index 90886a3cad..d0de71695f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserController.java @@ -56,10 +56,10 @@ import org.jooq.DSLContext; -public final class WaterUserController extends WaterSupplyControllerBase implements Handler { +public final class WaterUserController extends WaterSupplyControllerBase { public WaterUserController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCreateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCreateController.java index 4516a7adaf..ff6e36fde9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCreateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserCreateController.java @@ -49,10 +49,10 @@ import org.jooq.DSLContext; -public final class WaterUserCreateController extends WaterSupplyControllerBase implements Handler { +public final class WaterUserCreateController extends WaterSupplyControllerBase { public WaterUserCreateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserDeleteController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserDeleteController.java index 2fece833ec..aaacb46333 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserDeleteController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserDeleteController.java @@ -50,11 +50,11 @@ import org.jooq.DSLContext; -public final class WaterUserDeleteController extends WaterSupplyControllerBase implements Handler { +public final class WaterUserDeleteController extends WaterSupplyControllerBase { public WaterUserDeleteController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserUpdateController.java b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserUpdateController.java index 32bdc4a9ea..1f69eff36b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserUpdateController.java +++ b/cwms-data-api/src/main/java/cwms/cda/api/watersupply/WaterUserUpdateController.java @@ -58,10 +58,10 @@ import org.jooq.DSLContext; -public final class WaterUserUpdateController extends WaterSupplyControllerBase implements Handler { +public final class WaterUserUpdateController extends WaterSupplyControllerBase { public WaterUserUpdateController(MetricRegistry metrics) { - waterMetrics(metrics); + super(metrics); } @OpenApi( diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobAccess.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobAccess.java new file mode 100644 index 0000000000..0616a6dfa3 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobAccess.java @@ -0,0 +1,22 @@ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.Blob; +import cwms.cda.data.dto.Blobs; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public interface BlobAccess { + @NotNull Blobs getBlobs(@Nullable String cursor, int pageSize, @Nullable String officeId, @Nullable String like); + + Optional getByUniqueName(String id, String office); + + void getBlob(String id, String office, StreamConsumer consumer, @Nullable Long offset, @Nullable Long end); + + void create(Blob blob, boolean failIfExists, boolean ignoreNulls); + + void update(Blob blob, boolean ignoreNulls); + + void delete(String office, String id); +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobDao.java index bdac0a54f8..c9c0861fa5 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/BlobDao.java @@ -1,5 +1,7 @@ package cwms.cda.data.dao; +import com.google.common.flogger.FluentLogger; +import cwms.cda.api.RangeParser; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.Blob; import cwms.cda.data.dto.Blobs; @@ -14,6 +16,7 @@ import org.jooq.ResultQuery; import org.jooq.SelectLimitPercentStep; import org.jooq.Table; + import usace.cwms.db.jooq.codegen.packages.CWMS_TEXT_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_CWMS_MEDIA_TYPE; import usace.cwms.db.jooq.codegen.tables.AV_OFFICE; @@ -34,8 +37,8 @@ import static org.jooq.impl.DSL.table; import static org.jooq.impl.DSL.upper; -public class BlobDao extends JooqDao { - +public class BlobDao extends JooqDao implements BlobAccess { + static FluentLogger logger = FluentLogger.forEnclosingClass(); public static final String ID = "ID"; public static final String DESCRIPTION = "DESCRIPTION"; public static final String OFFICE_CODE = "OFFICE_CODE"; @@ -68,7 +71,7 @@ public Optional getByUniqueName(String id, String limitToOffice) { ResultQuery query; if (limitToOffice != null && !limitToOffice.isEmpty()) { queryStr = queryStr + " and CWMS_OFFICE.OFFICE_ID = ?"; - query = dsl.resultQuery(queryStr, id, limitToOffice); + query = dsl.resultQuery(queryStr, id, limitToOffice.toUpperCase()); } else { query = dsl.resultQuery(queryStr, id); } @@ -85,7 +88,9 @@ public Optional getByUniqueName(String id, String limitToOffice) { return Optional.ofNullable(retVal); } - public void getBlob(String id, String office, BlobConsumer consumer) { + @Override + public void getBlob(String id, String office, StreamConsumer consumer, @Nullable Long offset, @Nullable Long end) { + // Not using jOOQ here because we want the java.sql.Blob and not an automatic field binding. We want // blob so that we can pull out a stream to the data and pass that to javalin. // If the request included Content-Ranges Javalin can have the stream skip to the correct @@ -97,54 +102,97 @@ public void getBlob(String id, String office, BlobConsumer consumer) { // connection(dsl, connection -> { - try (PreparedStatement preparedStatement = connection.prepareStatement(BLOB_WITH_OFFICE)) { - preparedStatement.setString(1, office); - preparedStatement.setString(2, id); - try (ResultSet resultSet = preparedStatement.executeQuery()) { - if (resultSet.next()) { - String mediaType = resultSet.getString("MEDIA_TYPE_ID"); - java.sql.Blob blob = resultSet.getBlob("VALUE"); - try { - consumer.accept(blob, mediaType); - } finally { - if (blob != null) { - blob.free(); - } - } - } else { - throw new NotFoundException("Unable to find blob with id " + id + " in office " + office); - } + if(office == null ){ + try (PreparedStatement preparedStatement = connection.prepareStatement(BLOB_QUERY)) { + preparedStatement.setString(1, id); + executeAndHandle(consumer, offset, end, preparedStatement, "Unable to find blob with id " + id); + } + } else { + try (PreparedStatement preparedStatement = connection.prepareStatement(BLOB_WITH_OFFICE)) { + preparedStatement.setString(1, office.toUpperCase()); + preparedStatement.setString(2, id); + + executeAndHandle(consumer, offset, end, preparedStatement, "Unable to find blob with id " + id + " in office " + office); } } }); } - public void getBlob(String id, BlobConsumer consumer) { - - connection(dsl, connection -> { - try (PreparedStatement preparedStatement = connection.prepareStatement(BLOB_QUERY)) { - preparedStatement.setString(1, id); + private static void executeAndHandle(StreamConsumer consumer, @Nullable Long offset, @Nullable Long end, PreparedStatement preparedStatement, String message) throws SQLException, IOException { + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (resultSet.next()) { + handleResultSet(consumer, offset, end, resultSet); + } else { + throw new NotFoundException(message); + } + } + } - try (ResultSet resultSet = preparedStatement.executeQuery()) { - if (resultSet.next()) { - String mediaType = resultSet.getString("MEDIA_TYPE_ID"); - java.sql.Blob blob = resultSet.getBlob("VALUE"); - try { - consumer.accept(blob, mediaType); - } finally { - if (blob != null) { - blob.free(); - } - } - } else { - throw new NotFoundException("Unable to find blob with id " + id); - } + /** + * + * @param consumer + * @param offset where to start reading. 0 is first byte + * @param end position of last byte to include. inclusive. 0 would mean only return the first byte. + * @param resultSet + * @throws SQLException + * @throws IOException + */ + private static void handleResultSet(StreamConsumer consumer, @Nullable Long offset, @Nullable Long end, ResultSet resultSet) throws SQLException, IOException { + String mediaType = resultSet.getString("MEDIA_TYPE_ID"); + java.sql.Blob blob = resultSet.getBlob("VALUE"); + try { + long totalLength = blob.length(); + if (offset != null) { + long pos = offset + 1; // For getBinaryStream the first byte is at 1. + long length = getLength(offset, end, totalLength); + + logger.atFine().log("Reading blob at pos %s, length %s, totalLength %s", pos, length, totalLength); + try (InputStream stream = blob.getBinaryStream(pos, length)) { + consumer.accept(stream, offset, mediaType, totalLength); + } catch (SQLException e) { + logger.atWarning().withCause(e).log("Error reading blob at offset %s, length %s, totalLength %s", offset, length, totalLength); + throw e; + } + } else { + try (InputStream stream = blob.getBinaryStream()) { + consumer.accept(stream, 0, mediaType, totalLength); } } - }); + } finally { + blob.free(); + } + } + + /** + * + * @param offset the index of the first byte to read. Like http range-requests 0 is first byte. -1 is last byte. + * @param end the index of the last byte to read, inclusive. null reads until the end of the blob. -1 is also last byte. + * + * @param totalLength the total length of the blob + * @return the length of the range to read + */ + static long getLength(@NotNull Long offset, @Nullable Long end, long totalLength) { + + long length; + if(end != null){ + // The length we are getting passed in is from range-request and could be negative to indicate suffix + long[] startEnd = RangeParser.interpret(new long[]{offset, end}, totalLength); + if(startEnd != null){ + offset = startEnd[0]; + end = startEnd[1]; + } + + length = end - offset + 1; + } else { + // if its not set just assume we are reading until the end of blob. + // Consumer can always close stream early. + length = totalLength - offset; + } + return length; } + public List getAll(String officeId, String like) { String queryStr = "SELECT AT_BLOB.ID, AT_BLOB.DESCRIPTION, CWMS_MEDIA_TYPE.MEDIA_TYPE_ID, CWMS_OFFICE.OFFICE_ID\n" + " FROM CWMS_20.AT_BLOB \n" @@ -249,6 +297,7 @@ public List getAll(String officeId, String like) { return builder.build(); } + @Override public void create(Blob blob, boolean failIfExists, boolean ignoreNulls) { String pFailIfExists = formatBool(failIfExists); String pIgnoreNulls = formatBool(ignoreNulls); @@ -265,6 +314,7 @@ public void create(Blob blob, boolean failIfExists, boolean ignoreNulls) { blob.getOfficeId())); } + @Override public void update(Blob blob, boolean ignoreNulls) { String pFailIfExists = formatBool(false); String pIgnoreNulls = formatBool(ignoreNulls); @@ -288,6 +338,7 @@ public void update(Blob blob, boolean ignoreNulls) { blob.getOfficeId())); } + @Override public void delete(String office, String id) { if (!blobExists(office, id)) { throw new NotFoundException("Unable to find blob with id " + id + " in office " + office); @@ -336,8 +387,5 @@ public static byte[] readFully(@NotNull InputStream stream) throws IOException { return output.toByteArray(); } - @FunctionalInterface - public interface BlobConsumer { - void accept(java.sql.Blob blob, String mediaType) throws SQLException, IOException; - } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ClobDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ClobDao.java index c27b35348c..6debadcfc2 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/ClobDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ClobDao.java @@ -22,6 +22,7 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.Reader; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -50,7 +51,7 @@ public Optional getByUniqueName(String uniqueName, String office) { Condition cond = upper(vClob.ID).eq(upper(uniqueName)); if (office != null && !office.isEmpty()) { - cond = cond.and(vOffice.OFFICE_ID.eq(office)); + cond = cond.and(vOffice.OFFICE_ID.eq(office.toUpperCase())); } RecordMapper mapper = joinRecord -> @@ -75,14 +76,14 @@ public Clobs getClobs(String cursor, int pageSize, String officeLike, AV_OFFICE vOffice = AV_OFFICE.AV_OFFICE; Condition whereClause = JooqDao.caseInsensitiveLikeRegex(vClob.ID, idRegex) - .and(JooqDao.caseInsensitiveLikeRegexNullTrue(vOffice.OFFICE_ID, officeLike)); + .and(JooqDao.caseInsensitiveLikeRegexNullTrue(vOffice.OFFICE_ID, officeLike)); if (cursor == null || cursor.isEmpty()) { SelectConditionStep> count = dsl.select(count(asterisk())) - .from(vClob) - .join(vOffice).on(vClob.OFFICE_CODE.eq(vOffice.OFFICE_CODE)) - .where(whereClause); + .from(vClob) + .join(vOffice).on(vClob.OFFICE_CODE.eq(vOffice.OFFICE_CODE)) + .where(whereClause); Record1 rec = count.fetchOne(); - if(rec != null) { + if (rec != null) { total = rec.value1(); } } else { @@ -104,27 +105,27 @@ public Clobs getClobs(String cursor, int pageSize, String officeLike, Condition moreInSameOffice = cursorClobId == null || cursorOffice == null ? noCondition() : vOffice.OFFICE_ID.eq(cursorOffice.toUpperCase()) .and(upper(vClob.ID).greaterThan(cursorClobId.toUpperCase())); - Condition nextOffices = cursorOffice == null ? noCondition(): + Condition nextOffices = cursorOffice == null ? noCondition() : upper(vOffice.OFFICE_ID).greaterThan(cursorOffice.toUpperCase()); Condition pagingCondition = moreInSameOffice.or(nextOffices); SelectLimitPercentStep> query = dsl.select( - vOffice.OFFICE_ID, - vClob.ID, - vClob.DESCRIPTION, - includeValues ? vClob.VALUE : DSL.inline("").as(vClob.VALUE) - ) - .from(vClob) - .join(vOffice).on(vClob.OFFICE_CODE.eq(vOffice.OFFICE_CODE)) - .where(whereClause) - .and(pagingCondition) - .orderBy(vOffice.OFFICE_ID, vClob.ID) - .limit(pageSize); + vOffice.OFFICE_ID, + vClob.ID, + vClob.DESCRIPTION, + includeValues ? vClob.VALUE : DSL.inline("").as(vClob.VALUE) + ) + .from(vClob) + .join(vOffice).on(vClob.OFFICE_CODE.eq(vOffice.OFFICE_CODE)) + .where(whereClause) + .and(pagingCondition) + .orderBy(vOffice.OFFICE_ID, vClob.ID) + .limit(pageSize); Clobs.Builder builder = new Clobs.Builder(cursor, pageSize, total); - logger.atFine().log("%s", lazy(()->query.getSQL(ParamType.INLINED))); + logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); query.fetch().forEach(row -> { usace.cwms.db.jooq.codegen.tables.records.AV_CLOB clob = row.into(vClob); @@ -169,13 +170,13 @@ public void create(Clob clob, boolean failIfExists) { String pFailIfExists = getBoolean(failIfExists); connection(dsl, c -> - CWMS_TEXT_PACKAGE.call_STORE_TEXT( - getDslContext(c, clob.getOfficeId()).configuration(), - clob.getValue(), - clob.getId(), - clob.getDescription(), - pFailIfExists, - clob.getOfficeId())); + CWMS_TEXT_PACKAGE.call_STORE_TEXT( + getDslContext(c, clob.getOfficeId()).configuration(), + clob.getValue(), + clob.getId(), + clob.getDescription(), + pFailIfExists, + clob.getOfficeId())); } @NotNull @@ -190,8 +191,8 @@ public static String getBoolean(boolean failIfExists) { } public void delete(String officeId, String id) { - connection(dsl,c -> CWMS_TEXT_PACKAGE.call_DELETE_TEXT( - getDslContext(c,officeId).configuration(), id, officeId) + connection(dsl, c -> CWMS_TEXT_PACKAGE.call_DELETE_TEXT( + getDslContext(c, officeId).configuration(), id, officeId) ); } @@ -205,50 +206,47 @@ public void update(Clob clob, boolean ignoreNulls) { // it throws - ORA-20244: NULL_ARGUMENT: Argument P_TEXT is not allowed to be null // Also note: when pIgnoreNulls == 'F' and the value is "" (empty string) // it throws - ORA-20244: NULL_ARGUMENT: Argument P_TEXT is not allowed to be null - connection(dsl,c -> - CWMS_TEXT_PACKAGE.call_UPDATE_TEXT( - getDslContext(c,clob.getOfficeId()).configuration(), - clob.getValue(), - clob.getId(), - clob.getDescription(), - pIgnoreNulls, - clob.getOfficeId() - ) + connection(dsl, c -> + CWMS_TEXT_PACKAGE.call_UPDATE_TEXT( + getDslContext(c, clob.getOfficeId()).configuration(), + clob.getValue(), + clob.getId(), + clob.getDescription(), + pIgnoreNulls, + clob.getOfficeId() + ) ); } /** * - * @param clobId the id to search for - * @param officeId the office - * @param clobConsumer a consumer that should be handed the input stream and the length of the stream. + * @param clobId the id to search for + * @param officeId the office + * @param streamConsumer a consumer that should be handed the input stream and the length of the stream. */ - public void getClob(String clobId, String officeId, ClobConsumer clobConsumer) { - // Not using jOOQ here because we want the java.sql.Clob and not an automatic field binding. We want - // clob so that we can pull out a stream to the data and pass that to javalin. - // If the request included Content-Ranges Javalin can have the stream skip to the correct - // location, which will avoid reading unneeded data. Passing this stream right to the javalin - // response should let CDA return a huge (2Gb) clob to the client without ever holding the entire String - // in memory. - // We can't use the stream once the connection we get from jooq is closed, so we have to pass in - // what we want javalin to do with the stream as a consumer. - // - + public void getClob(String clobId, String officeId, StreamConsumer streamConsumer) { dsl.connection(connection -> { + // Not using jOOQ here because we want the java.sql.Clob and not an automatic field binding. We want + // clob so that we can pull out a stream to the data and pass that to javalin. + // If the request included Content-Ranges Javalin can have the stream skip to the correct + // location, which will avoid reading unneeded data. Passing this stream right to the javalin + // response should let CDA return a huge (2Gb) clob to the client without ever holding the entire String + // in memory. + // We can't use the stream once the connection we get from jooq is closed, so we have to pass in + // what we want javalin to do with the stream as a consumer. try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_CLOB_QUERY)) { - preparedStatement.setString(1, officeId); + preparedStatement.setString(1, officeId.toUpperCase()); preparedStatement.setString(2, clobId); try (ResultSet resultSet = preparedStatement.executeQuery()) { if (resultSet.next()) { java.sql.Clob clob = resultSet.getClob("VALUE"); + long length = clob.length(); - try { - clobConsumer.accept(clob); + try (InputStream is = clob.getAsciiStream()) { + streamConsumer.accept(is, 0, "text/plain", length); } finally { - if (clob != null) { - clob.free(); - } + clob.free(); } } else { throw new NotFoundException("Unable to find clob with id " + clobId + " in office " + officeId); @@ -259,19 +257,16 @@ public void getClob(String clobId, String officeId, ClobConsumer clobConsumer) { } public static String readFully(java.sql.Clob clob) throws IOException, SQLException { - try(Reader reader = clob.getCharacterStream(); - BufferedReader br = new BufferedReader(reader)) { + try (Reader reader = clob.getCharacterStream(); + BufferedReader br = new BufferedReader(reader)) { StringBuilder sb = new StringBuilder(); String line; - while(null != (line = br.readLine())) { + while (null != (line = br.readLine())) { sb.append(line); } return sb.toString(); } } - @FunctionalInterface - public interface ClobConsumer { - void accept(java.sql.Clob blob) throws SQLException, IOException; - } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/Dao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/Dao.java index 64de424ee8..80dc723076 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/Dao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/Dao.java @@ -31,7 +31,6 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.data.dto.CwmsDTO; import java.sql.Connection; -import java.sql.SQLException; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.jooq.DSLContext; @@ -43,6 +42,7 @@ public abstract class Dao { public static final int CWMS_18_1_8 = 180108; public static final int CWMS_21_1_1 = 210101; public static final int CWMS_23_03_16 = 230316; + public static final int CWMS_25_07_01 = 250701; public static final String PROP_BASE = "cwms.cda.data.dao.dao"; public static final String VERSION_NAME = "version"; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastInstanceDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastInstanceDao.java index 8b70042e8b..4a868a0244 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastInstanceDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastInstanceDao.java @@ -115,27 +115,49 @@ public ForecastInstanceDao(DSLContext dsl) { } public void create(ForecastInstance forecastInst) { - String officeId = forecastInst.getSpec().getOfficeId(); - Timestamp forecastDate = Timestamp.from(forecastInst.getDateTime()); - Timestamp issueDate = Timestamp.from(forecastInst.getIssueDateTime()); - String forecastInfo = mapToJson(forecastInst.getMetadata()); - byte[] fileData = forecastInst.getFileData(); - BLOB_FILE_T blob = new BLOB_FILE_T(); - blob.setFILENAME(forecastInst.getFilename()); - blob.setMEDIA_TYPE(forecastInst.getFileMediaType()); - blob.setDATA_ENTRY_DATE(OffsetDateTime.now()); - blob.setQUALITY_CODE(0L); - blob.setTHE_BLOB(fileData); connection(dsl, conn -> { - setOffice(conn, officeId); - DefaultBinding.THREAD_LOCAL.set(UTC_CALENDAR); - CWMS_FCST_PACKAGE.call_STORE_FCST(DSL.using(conn).configuration(), forecastInst.getSpec().getSpecId(), - forecastInst.getSpec().getDesignator(), forecastDate, issueDate, - "UTC", forecastInst.getMaxAge(), forecastInst.getNotes(), forecastInfo, - blob, "F", "T", officeId); + DSLContext ctx = getDslContext(conn, forecastInst.getSpec().getOfficeId()); + store(ctx, forecastInst); }); } + private void store(DSLContext ctx, ForecastInstance forecastInst) { + String officeId = forecastInst.getSpec().getOfficeId(); + Timestamp forecastDate = Timestamp.from(forecastInst.getDateTime()); + Timestamp issueDate = Timestamp.from(forecastInst.getIssueDateTime()); + String forecastInfo = mapToJson(forecastInst.getMetadata()); + byte[] fileData = forecastInst.getFileData(); + BLOB_FILE_T blob = new BLOB_FILE_T(); + blob.setFILENAME(forecastInst.getFilename()); + blob.setMEDIA_TYPE(forecastInst.getFileMediaType()); + blob.setDATA_ENTRY_DATE(OffsetDateTime.now()); + blob.setQUALITY_CODE(0L); + blob.setTHE_BLOB(fileData); + DefaultBinding.THREAD_LOCAL.set(UTC_CALENDAR); + // Ensure delete + store occur in a single transaction so a store failure rolls back the delete + ctx.transaction(configuration -> { + DSLContext tx = DSL.using(configuration); + clearExistingForecastInstance(tx, forecastInst, officeId, forecastDate, issueDate); + CWMS_FCST_PACKAGE.call_STORE_FCST(configuration, forecastInst.getSpec().getSpecId(), + forecastInst.getSpec().getDesignator(), forecastDate, issueDate, + "UTC", forecastInst.getMaxAge(), forecastInst.getNotes(), forecastInfo, + blob, "F", "T", officeId); + }); + } + + private void clearExistingForecastInstance(DSLContext ctx, ForecastInstance forecastInst, String officeId, Timestamp forecastDate, Timestamp issueDate) { + ReplaceUtils.OperatorBuilder noopUrlBuilder = new ReplaceUtils.OperatorBuilder().withTemplate("") + .withOperatorKey("{noop}"); + try { + // If the instance doesn't exist this will throw a NotFoundException which we can ignore, if it does exist we want to delete it before storing the new one + getForecastInstance(ctx, 0, noopUrlBuilder, officeId, forecastInst.getSpec().getSpecId(), forecastInst.getSpec().getDesignator(), forecastDate.toInstant(), issueDate.toInstant()); + CWMS_FCST_PACKAGE.call_DELETE_FCST(ctx.configuration(), forecastInst.getSpec().getSpecId(), forecastInst.getSpec().getDesignator(), + forecastDate, issueDate, "UTC", officeId); + } catch (NotFoundException e) { + // nothing to delete + } + } + private static String mapToJson(Map metadata) { if(metadata == null) { return null; @@ -149,7 +171,7 @@ private static String mapToJson(Map metadata) { private static Map mapFromJson(String forecastInfo) { try { - return JsonV2.buildObjectMapper().readValue(forecastInfo, new TypeReference>() { + return JsonV2.buildObjectMapper().readValue(forecastInfo, new TypeReference<>() { }); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Error serializing forecast info to JSON", e); @@ -157,7 +179,12 @@ private static Map mapFromJson(String forecastInfo) { } public List getForecastInstances(int byteLimit, ReplaceUtils.OperatorBuilder urlBuilder, - String office, String name, String designator) { + String officeArg, String name, String designator) { + + if(officeArg != null){ + officeArg = officeArg.toUpperCase(); + } + String office = officeArg; String query = INSTANCE_QUERY + GET_ALL_CONDITIONS; return connectionResult(dsl, (Connection c) -> { @@ -264,10 +291,23 @@ private static ForecastInstance map(int byteLimit, ReplaceUtils.OperatorBuilder } public ForecastInstance getForecastInstance(int byteLimit, ReplaceUtils.OperatorBuilder urlBuilder, - String office, String name, String designator, + String officeArg, String name, String designator, Instant forecastDate, Instant issueDate) { - String query = INSTANCE_QUERY + GET_ONE_CONDITIONS; return connectionResult(dsl, c -> { + DSLContext ctx = getDslContext(c, officeArg); + return getForecastInstance(ctx, byteLimit, urlBuilder, officeArg, name, designator, forecastDate, issueDate); + }); + } + + private static ForecastInstance getForecastInstance(DSLContext ctx, int byteLimit, ReplaceUtils.OperatorBuilder urlBuilder, String officeArg, + String name, String designator, Instant forecastDate, Instant issueDate) { + if(officeArg != null){ + officeArg = officeArg.toUpperCase(); + } + String office = officeArg; + + String query = INSTANCE_QUERY + GET_ONE_CONDITIONS; + return connectionResult(ctx, c -> { try (PreparedStatement preparedStatement = c.prepareStatement(query)) { preparedStatement.setString(1, office); preparedStatement.setString(2, name); @@ -295,11 +335,15 @@ public void update(ForecastInstance forecastInst) { String designator = forecastInst.getSpec().getDesignator(); Instant forecastDate = forecastInst.getDateTime(); Instant issueDate = forecastInst.getIssueDateTime(); - //Will throw a NotFoundException if instance doesn't exist - ReplaceUtils.OperatorBuilder noopUrlBuilder = new ReplaceUtils.OperatorBuilder().withTemplate("") - .withOperatorKey("{noop}"); - getForecastInstance(0, noopUrlBuilder, officeId, specId, designator, forecastDate, issueDate); - create(forecastInst); + connection(dsl, c -> { + DSLContext ctx = getDslContext(c, forecastInst.getSpec().getOfficeId()); + //Will throw a NotFoundException if instance doesn't exist + ReplaceUtils.OperatorBuilder noopUrlBuilder = new ReplaceUtils.OperatorBuilder().withTemplate("") + .withOperatorKey("{noop}"); + getForecastInstance(ctx, 0, noopUrlBuilder, officeId, specId, designator, forecastDate, issueDate); + store(ctx, forecastInst); + }); + } public void delete(String office, String name, String designator, @@ -314,7 +358,7 @@ public void delete(String office, String name, String designator, } public void getFileBlob(String office, String name, String designator, - Instant forecastDate, Instant issueDate, BlobDao.BlobConsumer consumer) { + Instant forecastDate, Instant issueDate, StreamConsumer consumer) { String query = FILE_QUERY + FILE_CONDITIONS; connection(dsl, c -> { @@ -337,8 +381,8 @@ public void getFileBlob(String office, String name, String designator, } Blob blob = (Blob) attributes[5]; - try { - consumer.accept(blob, mediaType); + try (InputStream is = blob.getBinaryStream()){ + consumer.accept(is, 0, mediaType, blob.length()); return; } finally { if (blob != null) { @@ -348,7 +392,8 @@ public void getFileBlob(String office, String name, String designator, } } } - consumer.accept(null, null); + // If we get here there was some problem finding the stream. + throw new NotFoundException("Forecast Instance file not found"); } } }); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastSpecDao.java index e11af998d3..c6be63f124 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ForecastSpecDao.java @@ -2,15 +2,17 @@ import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dto.forecast.ForecastSpec; -import cwms.cda.formatters.UnsupportedFormatException; +import org.jetbrains.annotations.NotNull; import org.jooq.SelectConditionStep; +import org.jooq.TableField; import usace.cwms.db.jooq.codegen.packages.CWMS_FCST_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_FCST_LOCATION; import usace.cwms.db.jooq.codegen.tables.AV_FCST_SPEC; import usace.cwms.db.jooq.codegen.tables.AV_FCST_TIME_SERIES; import org.jooq.DSLContext; +import org.jooq.Condition; import org.jooq.Record2; import org.jooq.Record7; import org.jooq.SelectOnConditionStep; @@ -56,13 +58,13 @@ public void delete(String office, String specId, String designator, DeleteRule d } public List getForecastSpecs(String office, String specIdRegex, - String designator, String sourceEntity) { + String designator, String sourceEntityRegex, String entityLike) { AV_FCST_SPEC spec = AV_FCST_SPEC.AV_FCST_SPEC; SelectConditionStep> query = forecastSpecQuery(dsl) .where(JooqDao.caseInsensitiveLikeRegex(spec.OFFICE_ID, office)) .and(JooqDao.caseInsensitiveLikeRegex(spec.FCST_SPEC_ID, specIdRegex)) - .and(JooqDao.caseInsensitiveLikeRegex(spec.ENTITY_ID, sourceEntity)); + .and(buildEntityCondition(spec, sourceEntityRegex, entityLike)); //Designator is a nullable column in the database. if(designator == null) { query = query.and(spec.FCST_DESIGNATOR.isNull()); @@ -73,6 +75,17 @@ public List getForecastSpecs(String office, String specIdRegex, .map(ForecastSpecDao::map); } + private Condition buildEntityCondition(AV_FCST_SPEC spec, + String sourceEntityRegex, + String entityLike) { + // If entityLike is provided, use case-insensitive LIKE + if (entityLike != null) { + return spec.ENTITY_ID.likeIgnoreCase(entityLike); + } + // Fallback to regex behavior + return JooqDao.caseInsensitiveLikeRegex(spec.ENTITY_ID, sourceEntityRegex); + } + private static SelectOnConditionStep> forecastSpecQuery(DSLContext dsl) { AV_FCST_SPEC spec = AV_FCST_SPEC.AV_FCST_SPEC; @@ -111,11 +124,11 @@ private static ForecastSpec map(Record7> query = forecastSpecQuery(dsl) - .where(spec.OFFICE_ID.eq(office)) + .where(spec.OFFICE_ID.eq(office.toUpperCase())) .and(spec.FCST_SPEC_ID.eq(name)); if(designator != null) { query = query.and(spec.FCST_DESIGNATOR.eq(designator)); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java index 62b0a1df1a..9d95653e73 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/JooqDao.java @@ -27,13 +27,13 @@ import static org.jooq.SQLDialect.ORACLE; import com.google.common.flogger.FluentLogger; -import com.google.common.flogger.StackSize; import cwms.cda.ApiServlet; import cwms.cda.api.errors.AlreadyExists; import cwms.cda.api.errors.FieldLengthExceededException; import cwms.cda.api.errors.InvalidItemException; import cwms.cda.api.errors.NotFoundException; import cwms.cda.datasource.ConnectionPreparingDataSource; +import cwms.cda.datasource.DelegatingConnectionPreparer; import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; import cwms.cda.security.CwmsAuthException; import io.javalin.http.Context; @@ -51,6 +51,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -71,8 +72,7 @@ import org.jooq.impl.DefaultExecuteListenerProvider; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; -import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; -import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; public abstract class JooqDao extends Dao { @@ -87,7 +87,7 @@ public abstract class JooqDao extends Dao { static ExecuteListener listener = new ExceptionWrappingListener(); private static final Pattern INVALID_OFFICE_ID = Pattern.compile( - "INVALID_OFFICE_ID: \"([^\"]+)\" is not a valid CWMS office id"); + "INVALID_OFFICE_ID: \"([^\"]+)\" is not a valid CWMS office id"); private static final Pattern INVALID_UNIT = Pattern.compile( "(.+\\R+){6}ORA-20102: The unit: \\S+" + " is not a recognized CWMS Database unit for the .+(.+\\R+){10}"); @@ -133,34 +133,22 @@ protected JooqDao(DSLContext dsl) { */ public static DSLContext getDslContext(Context ctx) { DSLContext retVal; - final String officeId = ctx.attribute(ApiServlet.OFFICE_ID); + final DataSource dataSource = ctx.attribute(ApiServlet.DATA_SOURCE); - final Boolean isNewLRTS = ctx.header(ApiServlet.IS_NEW_LRTS) == null - ? null : Boolean.parseBoolean(ctx.header(ApiServlet.IS_NEW_LRTS)); + final boolean isNewLRTS = ctx.header(ApiServlet.IS_NEW_LRTS) != null && Boolean.parseBoolean(ctx.header(ApiServlet.IS_NEW_LRTS)); - if (dataSource != null) { - DataSource wrappedDataSource = new ConnectionPreparingDataSource(connection -> - setClientInfo(ctx, connection), dataSource); - retVal = DSL.using(wrappedDataSource, SQLDialect.ORACLE18C); - } else { - // Some tests still use this method - logger.atFine().withStackTrace(StackSize.FULL) - .log("System still using old context method."); - Connection database = ctx.attribute(ApiServlet.DATABASE); - retVal = getDslContext(database, officeId); - } + DelegatingConnectionPreparer preparer = new DelegatingConnectionPreparer( + connection -> setClientInfo(ctx, connection), + new cwms.cda.datasource.LrtsSessionPreparer(isNewLRTS)); + DataSource wrappedDataSource = new ConnectionPreparingDataSource(preparer, dataSource); + retVal = DSL.using(wrappedDataSource, SQLDialect.ORACLE18C); retVal.configuration().set(new DefaultExecuteListenerProvider(listener)); - if (isNewLRTS != null) { - String requireBool = isNewLRTS ? "T" : "F"; - int requireIntValue = isNewLRTS ? REQUIRE_NEW_LRTS_ID_FORMAT : REQUIRE_OLD_LRTS_ID_FORMAT; - CWMS_UTIL_PACKAGE.call_SET_SESSION_INFO(retVal.configuration(), - SESSION_USE_LRTS_ID_FORMAT, requireBool, requireIntValue); - } return retVal; } + protected static Timestamp buildTimestamp(Instant date) { return date != null ? Timestamp.from(date) : null; } @@ -184,8 +172,8 @@ private static Connection setClientInfo(Context ctx, Connection connection) { try { final String apiVersion = ApiServlet.getApiVersion(); connection.setClientInfo("OCSID.ECID", - ApiServlet.APPLICATION_TITLE + " " + - apiVersion.substring(0,Math.min(ORACLE_ECID_MAX_LENGTH,apiVersion.length()))); + ApiServlet.APPLICATION_TITLE + " " + + apiVersion.substring(0, Math.min(ORACLE_ECID_MAX_LENGTH, apiVersion.length()))); if (ctx.handlerType() == HandlerType.BEFORE) { connection.setClientInfo("OCSID.MODULE", "BEFORE-HANDLER"); } else { @@ -215,12 +203,44 @@ protected static Double toDouble(BigDecimal bigDecimal) { return retVal; } + /** + * The idea here is that this will check the current default datum, + * possible switch to the specified datum and + * then run the code and + * if the datum was previously switched + * then switch back to the initial datum. + * + * @param targetDatum The desired ver + * @param dslContext + * @param cr + */ + protected void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { + String defaultVertDatum = CWMS_LOC_PACKAGE.call_GET_DEFAULT_VERTICAL_DATUM(dslContext.configuration()); + String targetName = (targetDatum != null) ? targetDatum.toString() : null; + boolean changeDefaultDatum = !Objects.equals(targetName, defaultVertDatum); + try { + if (changeDefaultDatum) { + CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), targetName); + } + + connection(dslContext, cr); + } finally { + if (changeDefaultDatum) { + // If we changed it we should restore. + CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), defaultVertDatum); + } + } + } + /** * Oracle supports case insensitive regexp search but the syntax for calling it is a * bit weird. This method lets Dao classes add a case-insensitive regexp search in * an easy to read manner without having to worry about the syntax. */ public static Condition caseInsensitiveLikeRegex(Field field, String regex) { + if ("*".equals(regex) || ".*".equals(regex)) { + return DSL.noCondition(); + } return new CustomCondition() { @Override public void accept(org.jooq.Context ctx) { @@ -269,6 +289,7 @@ public static Condition caseInsensitiveLikeRegexNullTrue(Field field, St * is one of several types of exception (e.q NotFound, * AlreadyExists, NullArg) that can be specially handled by ApiServlet * by returning specific HTTP codes or error messages. + * * @param input the observed exception * @return An exception, possibly wrapped */ @@ -327,7 +348,7 @@ private static boolean hasCodeOrMessage(SQLException sqlException, } private static boolean hasCodeAndMessage(SQLException sqlException, - List codes, List segments) { + List codes, List segments) { final String localizedMessage = sqlException.getLocalizedMessage(); return codes.contains(sqlException.getErrorCode()) @@ -361,26 +382,26 @@ public static boolean isNotFound(RuntimeException input) { public static boolean isInvalidOffice(RuntimeException input) { return getSqlException(input) - .map(sqlException -> hasCodeOrMessage(sqlException, Collections.singletonList(20010), - Collections.singletonList("INVALID_OFFICE_ID"))) - .orElse(false); + .map(sqlException -> hasCodeOrMessage(sqlException, Collections.singletonList(20010), + Collections.singletonList("INVALID_OFFICE_ID"))) + .orElse(false); } public static boolean isValueTooLargeException(RuntimeException input) { return getSqlException(input.getCause()).map(sqlException -> hasCodeOrMessage(sqlException, - Arrays.asList(6502, 12899, 20041), - Arrays.asList("value too large for column", "character string buffer too small", - "Error while writing value at JDBC bind index:"))) - .orElse(false); + Arrays.asList(6502, 12899, 20041), + Arrays.asList("value too large for column", "character string buffer too small", + "Error while writing value at JDBC bind index:"))) + .orElse(false); } public static boolean isTSIDInvalidIntervalException(RuntimeException input) { return getSqlException(input.getCause()).map(sqlException -> hasCodeAndMessage(sqlException, - Arrays.asList(20205, 20998), - Arrays.asList("Invalid Time Series Description", - "is not a valid interval", - "INVALID Time Series Identifier"))) - .orElse(false); + Arrays.asList(20205, 20998), + Arrays.asList("Invalid Time Series Description", + "is not a valid interval", + "INVALID Time Series Identifier"))) + .orElse(false); } static InvalidItemException buildInvalidTSIDIntervalException(RuntimeException input) { @@ -395,12 +416,11 @@ static InvalidItemException buildInvalidTSIDIntervalException(RuntimeException i if (localizedMessage != null) { String[] parts = localizedMessage.split("\n"); String errorMessage = parts[0]; - if (CURRENT_SCHEMA_VERSION <= SCHEMA_VERSION.V2025_07_01.numeric() && parts.length > 2) - { + if (CURRENT_SCHEMA_VERSION <= SCHEMA_VERSION.V2025_07_01.numeric() && parts.length > 2) { errorMessage = parts[1]; } - return new InvalidItemException(String.format("Invalid time series description: %s", - errorMessage), cause); + return new InvalidItemException(String.format("Invalid time series description: %s", + errorMessage), cause); } return new InvalidItemException("Invalid time series description", cause); } @@ -469,7 +489,7 @@ static CwmsAuthException buildNotAuthorizedForOffice(RuntimeException input) { } return new CwmsAuthException("User not authorized for this office.", cause, - HttpServletResponse.SC_UNAUTHORIZED); + HttpServletResponse.SC_UNAUTHORIZED); } public static boolean isAlreadyExists(RuntimeException input) { @@ -696,12 +716,40 @@ public static boolean isUnsupportedOperationException(RuntimeException input) { int errorCode = sqlException.getErrorCode(); //procedure doesn't exist retVal = errorCode == 904 - //Table or view does not exist - || errorCode == 942; + //Table or view does not exist + || errorCode == 942; } return retVal; } + /** + * Returns true if: + * - PL/SQL procedure doesn't exist + * - or it exists but fails to bind (signature mismatch) + * + */ + public static boolean isMissingOrBindFailure(Throwable t) { + Optional sqlExOpt = JooqDao.getSqlException(t); + if (sqlExOpt.isEmpty()) { + return false; + } + + SQLException sqlEx = sqlExOpt.get(); + String msg = sqlEx.getMessage(); + if (msg == null) { + msg = ""; + } + msg = msg.toUpperCase(); + + // Example: + // - ORA-06550 ... PLS-00302: component 'DELETE_TS_GROUP_CASCADE' must be declared + return msg.contains("PLS-00302") + || msg.contains("PLS-00306") + || msg.contains("ORA-04043") + || msg.contains("ORA-06550") + || msg.contains("ORA-00904"); + } + private static UnsupportedOperationException buildUnsupportedOperationException(RuntimeException input) { Throwable cause = input; @@ -732,8 +780,9 @@ private static UnsupportedOperationException buildUnsupportedOperationException( /** * JooqDao provides its own connection() which wraps throw exceptions * because the DSL.connection() method does not wrap exceptions. + * * @param dslContext the DSLContext to use - * @param cr the ConnectionRunnable to run with the connection + * @param cr the ConnectionRunnable to run with the connection */ protected static void connection(DSLContext dslContext, ConnectionRunnable cr) { try { @@ -747,8 +796,9 @@ protected static void connection(DSLContext dslContext, ConnectionRunnable cr) { * Like DSL.connection the DSL.connectionResult method does not cause thrown * exceptions to be wrapped. This method delegates to DSL.connectionResult * but will wrap exceptions into more specific exception types where possible. + * * @param dslContext the DSLContext to use - * @param var1 the ConnectionCallable to run with the connection + * @param var1 the ConnectionCallable to run with the connection */ protected static R connectionResult(DSLContext dslContext, ConnectionCallable var1) { try { @@ -776,7 +826,7 @@ protected static ZoneId toZoneId(String zoneId, String locationId) { } else { if (logger.atFine().isEnabled()) { logger.atWarning().withCause(e) - .log("Location %s has an invalid location time zone: %s", locationId, zoneId); + .log("Location %s has an invalid location time zone: %s", locationId, zoneId); } else { logger.atWarning().log("Location %s has an invalid location time zone: %s", locationId, zoneId); } @@ -788,7 +838,7 @@ protected static ZoneId toZoneId(String zoneId, String locationId) { public static BigDecimal toBigDecimal(Number number) { return (number == null) ? null : BigDecimal.valueOf( - number.doubleValue()); + number.doubleValue()); } public static double buildDouble(BigDecimal bigDecimal) { @@ -796,7 +846,7 @@ public static double buildDouble(BigDecimal bigDecimal) { } protected static void checkMetaData(ResultSetMetaData metaData, List columnList, - String type) throws SQLException { + String type) throws SQLException { int columnCount = metaData.getColumnCount(); List metadataColumns = new ArrayList<>(); logger.atFine().log("{0} column dump.", type); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationCategoryDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationCategoryDao.java index e743f4495c..6427805e1b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationCategoryDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationCategoryDao.java @@ -27,6 +27,8 @@ import cwms.cda.data.dto.LocationCategory; import java.util.List; import java.util.Optional; + +import org.jetbrains.annotations.NotNull; import org.jooq.DSLContext; import org.jooq.Record3; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; @@ -57,17 +59,17 @@ public List getLocationCategories(String officeId) { return dsl.selectDistinct(table.CAT_DB_OFFICE_ID, table.LOC_CATEGORY_ID, table.LOC_CATEGORY_DESC) .from(table) - .where(table.CAT_DB_OFFICE_ID.eq(officeId)) + .where(table.CAT_DB_OFFICE_ID.eq(officeId.toUpperCase())) .fetch().into(LocationCategory.class); } - public Optional getLocationCategory(String officeId, String categoryId) { + public Optional getLocationCategory(@NotNull String officeId, String categoryId) { AV_LOC_CAT_GRP table = AV_LOC_CAT_GRP.AV_LOC_CAT_GRP; Record3 fetchOne = dsl.selectDistinct(table.CAT_DB_OFFICE_ID, table.LOC_CATEGORY_ID, table.LOC_CATEGORY_DESC) .from(table) - .where(table.CAT_DB_OFFICE_ID.eq(officeId) + .where(table.CAT_DB_OFFICE_ID.eq(officeId.toUpperCase()) .and(table.LOC_CATEGORY_ID.eq(categoryId))) .fetchOne(); return fetchOne != null ? diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java index 2cb07af516..ec8352873a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationGroupDao.java @@ -90,6 +90,7 @@ public LocationGroupDao(DSLContext dsl) { */ public Optional getLocationGroup(@NotNull String officeId, @NotNull String categoryId, @NotNull String groupId) { + officeId = officeId.toUpperCase(); Condition joinCondition; if (CWMS.equalsIgnoreCase(officeId)) { @@ -344,6 +345,7 @@ public List getLocationGroups(String locationOfficeId, String gro Condition joinCondition = noCondition(); if (locationOfficeId != null) { + locationOfficeId = locationOfficeId.toUpperCase(); if (CWMS.equalsIgnoreCase(locationOfficeId)) { whereCondition = whereCondition.and(catGroupView.CAT_DB_OFFICE_ID.eq(CWMS) .and(catGroupView.GRP_DB_OFFICE_ID.eq(CWMS)) @@ -382,11 +384,11 @@ private List getGroupsWithoutAssignedLocations( Condition condition = catGroupView.LOC_GROUP_ID.isNotNull(); if (groupOfficeId != null && !groupOfficeId.isEmpty()) { - condition = condition.and(catGroupView.GRP_DB_OFFICE_ID.eq(groupOfficeId)); + condition = condition.and(catGroupView.GRP_DB_OFFICE_ID.eq(groupOfficeId.toUpperCase())); } if (categoryOfficeId != null && !categoryOfficeId.isEmpty()) { - condition = condition.and(catGroupView.CAT_DB_OFFICE_ID.eq(categoryOfficeId)); + condition = condition.and(catGroupView.CAT_DB_OFFICE_ID.eq(categoryOfficeId.toUpperCase())); } if (locCategoryLike != null && !locCategoryLike.isEmpty()) { @@ -436,9 +438,9 @@ public FeatureCollection buildFeatureCollectionForLocationGroup(String locationO groupAssignView.GROUP_ID, groupAssignView.ATTRIBUTE, groupAssignView.ALIAS_ID, groupAssignView.SHARED_REF_LOCATION_ID, groupAssignView.SHARED_ALIAS_ID) .from(al).join(groupAssignView).on(al.LOCATION_ID.eq(groupAssignView.LOCATION_ID)) - .where(groupAssignView.DB_OFFICE_ID.eq(locationOfficeId) - .and(groupAssignView.CATEGORY_OFFICE_ID.eq(categoryOfficeId)) - .and(groupAssignView.GROUP_OFFICE_ID.eq(groupOfficeId)) + .where(groupAssignView.DB_OFFICE_ID.eq(locationOfficeId.toUpperCase()) + .and(groupAssignView.CATEGORY_OFFICE_ID.eq(categoryOfficeId.toUpperCase())) + .and(groupAssignView.GROUP_OFFICE_ID.eq(groupOfficeId.toUpperCase())) .and(groupAssignView.CATEGORY_ID.eq(categoryId) .and(groupAssignView.GROUP_ID.eq(groupId)) .and(al.UNIT_SYSTEM.eq(units)))) diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java index a749b0a3b9..2bf8b2eb28 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationLevelsDaoImpl.java @@ -76,6 +76,7 @@ import java.util.Optional; import java.util.Set; import com.google.common.flogger.FluentLogger; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -550,10 +551,10 @@ public LocationLevel retrieveLocationLevel(String locationLevelName, String pUni units = CWMS_UTIL_PACKAGE.call_GET_DEFAULT_UNITS(configuration, parameter, units); } else if (units == null) { - logger.atInfo().log("Getting default units for %s", parameter); + logger.atFine().log("Getting default units for %s", parameter); String defaultUnits = CWMS_UTIL_PACKAGE.call_GET_DEFAULT_UNITS( configuration, parameter, UnitSystem.SI.getValue()); - logger.atInfo().log("Default units are %s", defaultUnits); + logger.atFine().log("Default units are %s", defaultUnits); units = defaultUnits; } LOCATION_LEVEL_T level = CWMS_LEVEL_PACKAGE.call_RETRIEVE_LOCATION_LEVEL__2( @@ -804,7 +805,9 @@ private void parseLevels(Record r, Map build seasonalBuilder.withSeasonalValue(seasonalValue); seasonalBuilder.withInterpolateString(interp); if (timeInterval != null) { - seasonalBuilder.withIntervalMinutes(timeInterval.getMinutes()); + double totalMilli = timeInterval.getTotalMilli(); + long minutes = TimeUnit.MILLISECONDS.toMinutes((long) totalMilli); + seasonalBuilder.withIntervalMinutes((int) minutes); } seasonalBuilder.withAttributeParameterId(attrId); seasonalBuilder.withAttributeUnitsId(attrUnit); @@ -814,7 +817,7 @@ private void parseLevels(Record r, Map build seasonalBuilder = withLocationLevelRef(seasonalBuilder, locationLevelRef); JDomSeasonalIntervalImpl offset = new JDomSeasonalIntervalImpl(); offset.setYearMonthString(calendarInterval); - seasonalBuilder.withIntervalMonths(offset.getMonths()); + seasonalBuilder.withIntervalMonths(offset.getTotalMonths()); seasonalBuilder.withIntervalOrigin(intervalOrigin, levelZdt); seasonalBuilder.withAliases(aliases); seasonalBuilder.withExpirationDate(expireDate); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationVerticalDatumConverter.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationVerticalDatumConverter.java new file mode 100644 index 0000000000..caa1c9c55a --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationVerticalDatumConverter.java @@ -0,0 +1,49 @@ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.VerticalDatumInfo; + +import java.util.Objects; +import java.util.Optional; + +public final class LocationVerticalDatumConverter { + + private LocationVerticalDatumConverter() { + throw new AssertionError("Utility class, don't instantiate"); + } + + public static Location convertToVerticalDatum(Location originalLocation, VerticalDatum convertTo, VerticalDatumInfo vdi) { + if (originalLocation == null || convertTo == null) { + return originalLocation; + } + if (vdi == null) { + return originalLocation; + } + VerticalDatum current = getVerticalDatum(originalLocation).orElse(convertTo); + if (Objects.equals(convertTo, current)) { + return originalLocation; + } + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(convertTo); + if (offset == null) { + return originalLocation; + } + VerticalDatumInfo newVdi = vdi.convertedTo(offset); + return new Location.Builder(originalLocation) + .withElevation(newVdi.getElevation()) + .withElevationUnits(newVdi.getUnit()) + .withVerticalDatum(newVdi.getNativeDatum()) + .build(); + } + + public static Optional getVerticalDatum(Location location) { + return Optional.ofNullable(location) + .map(Location::getVerticalDatum) // unwrap Optional + .filter(s -> !s.isBlank()) + .map(s -> { + if (s.equalsIgnoreCase(VerticalDatum.OTHER.toString())) { + throw new IllegalArgumentException("Vertical Datum of OTHER is not currently supported."); + } + return VerticalDatum.getVerticalDatum(s); + }); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java index 3f8fddbe95..3c71ff2428 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDao.java @@ -38,7 +38,7 @@ public interface LocationsDao { List getLocationKinds(String idRegexMask, String kindRegexMask, String officeId); - Location getLocation(String locationName, String unitSystem, String officeId, boolean includeAliases) throws IOException; + Location getLocation(String locationName, String unitSystem, String officeId, boolean includeAliases, String datum) throws IOException; Location getLocation(String locationName, String unitSystem, String officeId) throws IOException; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java index 5580a311c5..806f489822 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/LocationsDaoImpl.java @@ -40,6 +40,7 @@ import static org.jooq.impl.DSL.count; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.noCondition; import static org.jooq.impl.DSL.select; import static usace.cwms.db.jooq.codegen.tables.AV_LOC.AV_LOC; import static usace.cwms.db.jooq.codegen.tables.AV_LOC_ALIAS.AV_LOC_ALIAS; @@ -53,6 +54,7 @@ import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.CwmsIdLocationKind; import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.VerticalDatumInfo; import cwms.cda.data.dto.catalog.CatalogEntry; import cwms.cda.data.dto.catalog.LocationAlias; import cwms.cda.data.dto.catalog.LocationCatalogEntry; @@ -83,6 +85,8 @@ import org.jooq.OrderField; import org.jooq.Record; import org.jooq.Record1; +import org.jooq.RecordMapper; +import org.jooq.Record4; import org.jooq.SelectConditionStep; import org.jooq.SelectSeekStepN; import org.jooq.Table; @@ -90,6 +94,7 @@ import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; +import usace.cwms.db.jooq.codegen.tables.AV_VERT_DATUM_OFFSET; import usace.cwms.db.jooq.codegen.tables.AV_LOC2; import usace.cwms.db.jooq.codegen.udt.records.LOCATION_OBJ_T; @@ -114,26 +119,96 @@ public String getLocations(String names, String format, String units, String dat } public List getLocations(String nameRegex, String unitSystem, String datum, String officeId) { + return connectionResult(dsl, c -> { + /** + * BEG NOTE: Do not Set the session context here, if it's not null it's for the query itself, + * not related to the users's session. + */ + DSLContext dslContext = getDslContext(c, null); - Condition whereCondition = JooqDao.caseInsensitiveLikeRegexNullTrue(AV_LOC.LOCATION_ID, nameRegex); + Condition whereCondition = JooqDao.caseInsensitiveLikeRegexNullTrue(AV_LOC.LOCATION_ID, nameRegex); - if (officeId != null) { - whereCondition = whereCondition.and(AV_LOC.DB_OFFICE_ID.equalIgnoreCase(officeId)); - } - - if (unitSystem != null) { - whereCondition = whereCondition.and(AV_LOC.UNIT_SYSTEM.equalIgnoreCase(unitSystem)); - } + if (officeId != null) { + whereCondition = whereCondition.and(AV_LOC.DB_OFFICE_ID.eq(officeId.toUpperCase())); + } - if (datum != null) { - whereCondition = whereCondition.and(AV_LOC.VERTICAL_DATUM.equalIgnoreCase(datum)); - } + if (unitSystem != null) { + whereCondition = whereCondition.and(AV_LOC.UNIT_SYSTEM.equalIgnoreCase(unitSystem)); + } - return dsl.select(AV_LOC.asterisk()) + List results = dslContext.select(AV_LOC.asterisk()) .from(AV_LOC) .where(whereCondition) .fetchSize(DEFAULT_SMALL_FETCH_SIZE) .fetch(this::buildLocation); + + List finalizedResults = new ArrayList<>(); + if (datum != null && !datum.isBlank()) { + for(Location loc : results) { + loc = convertLocationToVerticalDatum(dslContext, loc, datum, officeId); + finalizedResults.add(loc); + } + } + return finalizedResults; + }); + } + + public static Location convertLocationToVerticalDatum(DSLContext ctx, Location loc, String datum, String officeId) { + if (loc == null || datum == null || datum.isBlank()) { + return loc; + } + + // Determine the location's native vertical datum (stored on the Location) + String nativeDatum = loc.getVerticalDatum(); + if (nativeDatum == null || nativeDatum.isBlank()) { + return loc; // nothing to convert from + } + + // Query offsets from the materialized view for this location/office using a jOOQ mapper + String nativeDatumNormalized = nativeDatum.replace("-", "").toUpperCase(); + List offsets = ctx.select( + AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.VERTICAL_DATUM_ID_1, + AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.VERTICAL_DATUM_ID_2, + AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFSET, + AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.DESCRIPTION) + .from(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET) + .where(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFICE_ID.eq(officeId)) + .and(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.LOCATION_ID.eq(loc.getName())) + // Filter rows so that VERTICAL_DATUM_ID_1 matches the native datum (ignoring dashes/case) + .and(DSL.upper( + DSL.replace(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.VERTICAL_DATUM_ID_1, "-", "")) + .eq(nativeDatumNormalized)) + .fetch(verticalDatumOffsetMapper()); + + if (offsets.isEmpty()) { + return loc; // no offsets available -> no conversion + } + + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(officeId) + .withLocation(loc.getName()) + .withUnit(loc.getElevationUnits()) + .withNativeDatum(nativeDatum) + .withElevation(loc.getElevation()) + .withOffsets(offsets.toArray(new VerticalDatumInfo.Offset[0])) + .build(); + + VerticalDatum target = VerticalDatum.getVerticalDatum(datum); + if (target != null) { + loc = LocationVerticalDatumConverter.convertToVerticalDatum(loc, target, vdi); + } + return loc; + } + + + private static RecordMapper, VerticalDatumInfo.Offset> verticalDatumOffsetMapper() { + return r -> { + String toDatum = r.get(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.VERTICAL_DATUM_ID_2); + Double value = r.get(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFSET); + String desc = r.get(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.DESCRIPTION); + boolean estimate = desc != null && desc.toLowerCase().contains("estimate"); + return new VerticalDatumInfo.Offset(estimate, toDatum, value); + }; } @Override @@ -156,39 +231,48 @@ public List getLocationKinds(String idRegexMask, String kind @Override public Location getLocation(String locationName, String unitSystem, String officeId) { - return getLocation(locationName, unitSystem, officeId, false); + return getLocation(locationName, unitSystem, officeId, false, null); } @Override - public Location getLocation(String locationName, String unitSystem, String officeId, boolean includeAliases) { - if (includeAliases) { - List locs = dsl.select(asterisk()) - .from(AV_LOC2.AV_LOC2) - .leftJoin(AV_LOC_ALIAS) - .on(AV_LOC2.AV_LOC2.BASE_LOCATION_ID.eq(AV_LOC_ALIAS.BASE_LOCATION_ID).and( - AV_LOC2.AV_LOC2.LOCATION_CODE.eq(AV_LOC_ALIAS.LOCATION_CODE.cast(Long.class)))) - .where(AV_LOC2.AV_LOC2.DB_OFFICE_ID.equalIgnoreCase(officeId) - .and(AV_LOC2.AV_LOC2.UNIT_SYSTEM.equalIgnoreCase(unitSystem) - .and(AV_LOC2.AV_LOC2.LOCATION_ID.equalIgnoreCase(locationName)))) - .fetch(); - if (locs.isEmpty()) { - throw new NotFoundException("Location not found for office:" + officeId + " and unit " - + "system:" + unitSystem + " and id:" + locationName); + public Location getLocation(String locationName, String unitSystem, String officeId, boolean includeAliases, String datum) { + return connectionResult(dsl, c -> { + DSLContext dslContext = getDslContext(c, officeId); + + Location retVal; + if (includeAliases) { + List locs = dslContext.select(asterisk()) + .from(AV_LOC2.AV_LOC2) + .leftJoin(AV_LOC_ALIAS) + .on(AV_LOC2.AV_LOC2.BASE_LOCATION_ID.eq(AV_LOC_ALIAS.BASE_LOCATION_ID).and( + AV_LOC2.AV_LOC2.LOCATION_CODE.eq(AV_LOC_ALIAS.LOCATION_CODE.cast(Long.class)))) + .where(AV_LOC2.AV_LOC2.DB_OFFICE_ID.eq(officeId.toUpperCase()) + .and(AV_LOC2.AV_LOC2.UNIT_SYSTEM.equalIgnoreCase(unitSystem) + .and(AV_LOC2.AV_LOC2.LOCATION_ID.equalIgnoreCase(locationName)))) + .fetch(); + if (locs.isEmpty()) { + throw new NotFoundException("Location not found for office:" + officeId + " and unit " + + "system:" + unitSystem + " and id:" + locationName); + } + retVal = buildLocation(null, locs, true); + } else { + Record loc = dslContext.select(AV_LOC.asterisk()) + .from(AV_LOC) + .where(AV_LOC.DB_OFFICE_ID.eq(officeId.toUpperCase()) + .and(AV_LOC.UNIT_SYSTEM.equalIgnoreCase(unitSystem) + .and(AV_LOC.LOCATION_ID.equalIgnoreCase(locationName)))) + .fetchOne(); + if (loc == null) { + throw new NotFoundException("Location not found for office:" + officeId + " and unit " + + "system:" + unitSystem + " and id:" + locationName); + } + retVal = buildLocation(loc); } - return buildLocation(null, locs, true); - } else { - Record loc = dsl.select(AV_LOC.asterisk()) - .from(AV_LOC) - .where(AV_LOC.DB_OFFICE_ID.equalIgnoreCase(officeId) - .and(AV_LOC.UNIT_SYSTEM.equalIgnoreCase(unitSystem) - .and(AV_LOC.LOCATION_ID.equalIgnoreCase(locationName)))) - .fetchOne(); - if (loc == null) { - throw new NotFoundException("Location not found for office:" + officeId + " and unit " - + "system:" + unitSystem + " and id:" + locationName); + if(retVal != null && datum != null && !datum.isBlank()) { + retVal = convertLocationToVerticalDatum(dslContext, retVal, datum, officeId); } - return buildLocation(loc); - } + return retVal; + }); } private CwmsIdLocationKind buildLocationKind(Record loc) { @@ -301,9 +385,6 @@ public void deleteLocation(String locationName, String officeId, boolean cascade }); } - /** - * @deprecated Use {@link #storeLocation(Location, boolean)} instead. - */ @Deprecated @Override public void storeLocation(Location location) throws IOException { @@ -380,9 +461,16 @@ public FeatureCollection buildFeatureCollection(String names, String units, Stri units = "SI"; } + Condition whereCondition; + if (officeId != null) { + whereCondition = AV_LOC.DB_OFFICE_ID.eq(officeId.toUpperCase()); + } else { + whereCondition = noCondition(); + } + SelectConditionStep selectQuery = dsl.select(asterisk()) .from(AV_LOC) - .where(AV_LOC.DB_OFFICE_ID.eq(officeId)) + .where(whereCondition) .and(AV_LOC.UNIT_SYSTEM.eq(units)); if (names != null && !names.isEmpty()) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageBlobDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageBlobDao.java new file mode 100644 index 0000000000..07518a7c8d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageBlobDao.java @@ -0,0 +1,436 @@ +package cwms.cda.data.dao; + +import com.google.common.flogger.FluentLogger; +import cwms.cda.api.RangeParser; +import cwms.cda.api.errors.AlreadyExists; +import cwms.cda.api.errors.FieldLengthExceededException; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.Blob; +import cwms.cda.data.dto.Blobs; +import cwms.cda.data.dto.CwmsDTOPaginated; +import io.minio.*; +import io.minio.errors.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Pattern; + +import io.minio.messages.Item; + +/** + * Object Storage-backed implementation using MinIO Java client. keys like OFFICE/ID_UPPER. + */ +public class ObjectStorageBlobDao implements BlobAccess { + public static final String DESCRIPTION = "description"; + public static final String NO_SUCH_KEY = "NoSuchKey"; + FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final int ID_LENGTH_LIMIT = 256; // This is to match pl/sql limit + private static final int MAX_KEY_LENGTH = 1024; + private final ObjectStorageConfig config; + private final MinioClient client; + + public ObjectStorageBlobDao(ObjectStorageConfig config) { + this.config = config; + this.client = buildClient(config); + } + + private static MinioClient buildClient(ObjectStorageConfig cfg) { + MinioClient.Builder b = MinioClient.builder(); + if (cfg.endpoint() != null && !cfg.endpoint().isEmpty()) { + b = b.endpoint(cfg.endpoint()); + } + if (cfg.accessKey() != null && cfg.secretKey() != null) { + b = b.credentials(cfg.accessKey(), cfg.secretKey()); + } + + return b.build(); + } + + @Override + public @NotNull Blobs getBlobs(@Nullable String cursor, int pageSize, @Nullable String officeId, @Nullable String like) { + String prefix = null; + if (officeId != null && !officeId.isEmpty()) { + prefix = officeId.toUpperCase(Locale.ROOT) + "/"; + } + + String startAfter = null; + + String cursorOffice = null; + String cursorId = null; + if (cursor != null && !cursor.isEmpty()) { + final String[] parts = CwmsDTOPaginated.decodeCursor(cursor, "||"); + + if (parts.length > 1) { + cursorOffice = Blobs.getOffice(cursor); + cursorId = Blobs.getId(cursor); + pageSize = Integer.parseInt(parts[2]); + } + + if (cursorOffice != null && cursorId != null) { + startAfter = buildName(cursorOffice, cursorId); + } + } + + Pattern likePattern = null; + if (like != null && !like.isEmpty() && !".*".equals(like)) { + likePattern = Pattern.compile(like, Pattern.CASE_INSENSITIVE); + } + List collected = getBlobs(pageSize, likePattern, prefix, startAfter); + + Blobs.Builder builder = new Blobs.Builder(cursor, pageSize, 0); + collected.forEach(builder::addBlob); + return builder.build(); + } + + private @NotNull List getBlobs(int pageSize, @Nullable Pattern likePattern, String prefix, String startAfter) { + List collected = new ArrayList<>(); + + ListObjectsArgs.Builder args = ListObjectsArgs.builder() + .bucket(requiredBucket()) + .recursive(true) + .maxKeys(pageSize); + if (prefix != null) { + args = args.prefix(prefix); + } + if (startAfter != null){ + args = args.startAfter(startAfter); + } + + for (Result res : client.listObjects(args.build())) { + try { + // item.key() like OFFICE/ID + Item item = res.get(); + String name = item.objectName(); + if(nameMatches(name, likePattern)) { + try { + Blob blob = getBlob(name); + collected.add(blob); + if (collected.size() >= pageSize) { + break; + } + } catch (Exception e) { + // skip items that fail stat + } + } + } + catch (Exception ignore) { + // skip this entry on error + } + } + return collected; + } + + private @NotNull Blob getBlob(String name) throws ErrorResponseException, InsufficientDataException, InternalException, InvalidKeyException, InvalidResponseException, IOException, NoSuchAlgorithmException, ServerException, XmlParserException { + StatObjectResponse stat = client.statObject(StatObjectArgs.builder() + .bucket(requiredBucket()) + .object(name) + .build()); + String mediaType = stat.contentType(); + String desc = stat.userMetadata() != null ? stat.userMetadata().getOrDefault(DESCRIPTION, null) : null; + return new Blob(officeFromName(name), idFromName(name), desc, mediaType, null); + } + + public static String officeFromName(String k){ + String off = null; + int slash = k.indexOf('/'); + if (slash > 0 && slash < k.length() - 1) { + off = k.substring(0, slash); + } + return off; + } + + public static String idFromName(String k) { + String id = null; + int slash = k.indexOf('/'); + if (slash > 0 && slash < k.length() - 1) { + id = k.substring(slash + 1); + } + return id; + } + + public static boolean nameMatches(String name, Pattern likePattern) { + boolean nameMatches = false; + + int slash = name.indexOf('/'); + if (slash > 0 && slash < name.length() - 1) { + String id = name.substring(slash + 1); + if (likePattern == null || likePattern.matcher(id).find()) { + nameMatches = true; + } + } + return nameMatches; + } + + + @Override + public Optional getByUniqueName(String id, String office) { + String k = (office == null || office.isEmpty()) ? findFirstKeyById(id) : buildName(office, id); + if (k == null) { + return Optional.empty(); + } + String officeFromKey = officeFromKey(k); + String idFromKey = idFromKey(k); + try { + StatObjectResponse stat = client.statObject(StatObjectArgs.builder() + .bucket(requiredBucket()) + .object(k) + .build()); + String mediaType = stat.contentType(); + String desc = stat.userMetadata() != null ? stat.userMetadata().getOrDefault(DESCRIPTION, null) : null; + return Optional.of(new Blob(officeFromKey, idFromKey, desc, mediaType, null)); + } catch (ErrorResponseException ere) { + if (NO_SUCH_KEY.equalsIgnoreCase(ere.errorResponse().code())) { + return Optional.empty(); + } + throw new RuntimeException(ere); + } catch (ServerException | InternalException | XmlParserException | InvalidResponseException | + InvalidKeyException | NoSuchAlgorithmException | IOException | InsufficientDataException e) { + throw new RuntimeException(e); + } + } + + @Override + public void getBlob(String id, String office, StreamConsumer consumer, @Nullable Long offset, @Nullable Long end) { + String key = (office == null || office.isEmpty()) ? findFirstKeyById(id) : buildName(office, id); + if (key == null) { + throw new NotFoundException("Could not find blob with id:" + id + " in office:" + office); + } + try { + logger.atFine().log("Getting stat for %s", key); + + StatObjectResponse stat = client.statObject(StatObjectArgs.builder() + .bucket(requiredBucket()) + .object(key) + .build()); + String mediaType = stat.contentType() != null ? stat.contentType() : "application/octet-stream"; + long totalLength = stat.size(); + + streamToConsumer(key, consumer, offset, end, mediaType, totalLength); + } catch (ErrorResponseException ere) { + if (NO_SUCH_KEY.equalsIgnoreCase(ere.errorResponse().code())) { + throw new NotFoundException("Could not find blob with id:" + id + " in office:" + office); + } + throw new RuntimeException(ere); + } catch (ServerException | InternalException | XmlParserException | InvalidResponseException | + InvalidKeyException | NoSuchAlgorithmException | IOException | InsufficientDataException | + SQLException e) { + throw new RuntimeException(e); + } + } + + private void streamToConsumer(String name, StreamConsumer consumer, @Nullable Long offset, @Nullable Long end, + String mediaType, long totalLength) throws SQLException, IOException { + + if(offset != null && end != null){ + long[] startEnd = RangeParser.interpret(new long[]{offset, end}, totalLength); + offset = startEnd[0]; + end = startEnd[1]; + } + + GetObjectArgs.Builder builder = GetObjectArgs.builder() + .bucket(requiredBucket()) + .object(name); + if(offset != null ) { + builder = builder.offset(offset); + } else { + offset = 0L; + } + + if(end != null && end > 0) { + long length = end - offset + 1; + builder = builder.length(length); + } + + try (InputStream is = client.getObject(builder.build())) { + consumer.accept(is, offset, mediaType, totalLength); + } catch (ServerException | InsufficientDataException e) { + throw new IOException(e); + } catch (InvalidKeyException e) { + throw new NotFoundException(e); + } catch (ErrorResponseException | NoSuchAlgorithmException | InvalidResponseException | XmlParserException | + InternalException e) { + throw new RuntimeException(e); + } + } + + + @Override + public void create(Blob blob, boolean failIfExists, boolean ignoreNulls) { + String name = buildName(blob.getOfficeId(), blob.getId()); + if (failIfExists) { + try { + client.statObject(StatObjectArgs.builder() + .bucket(requiredBucket()) + .object(name) + .build()); + throw new AlreadyExists("Blob already exists: " + name, null); + } catch (ErrorResponseException ere) { + if (!NO_SUCH_KEY.equalsIgnoreCase(ere.errorResponse().code())) { + throw new RuntimeException(ere); + } + } catch (ServerException | InsufficientDataException | IOException | NoSuchAlgorithmException | + InternalException | XmlParserException | InvalidResponseException | InvalidKeyException e) { + throw new RuntimeException(e); + } + } + + try { + doPut(blob, name, ignoreNulls); + } catch (ServerException | InsufficientDataException | ErrorResponseException | NoSuchAlgorithmException | + InvalidKeyException | InvalidResponseException | XmlParserException | InternalException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void update(Blob blob, boolean ignoreNulls) { + String name = buildName(blob.getOfficeId(), blob.getId()); + // For update make sure it exists first + try { + client.statObject(StatObjectArgs.builder() + .bucket(requiredBucket()) + .object(name) + .build()); + doPut(blob, name, ignoreNulls); + } catch (ErrorResponseException ere) { + if (NO_SUCH_KEY.equalsIgnoreCase(ere.errorResponse().code())) { + throw new NotFoundException("Unable to find blob with id " + blob.getId() + " in office " + blob.getOfficeId()); + } + throw new RuntimeException(ere); + } catch (ServerException | IOException | InsufficientDataException | NoSuchAlgorithmException | + InvalidKeyException | InvalidResponseException | XmlParserException | InternalException e) { + throw new RuntimeException(e); + } + } + + private void doPut(Blob blob, String name, boolean ignoreNulls) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { + byte[] value = blob.getValue(); + if (value == null && ignoreNulls) { + return; + } + + if (value == null) { + value = new byte[0]; + } + + try (InputStream is = new ByteArrayInputStream(value)) { + PutObjectArgs.Builder builder = PutObjectArgs.builder() + .bucket(requiredBucket()) + .object(name) + .stream(is, value.length, -1) + .contentType(blob.getMediaTypeId()); + + if (blob.getDescription() != null) { + builder.userMetadata(java.util.Collections.singletonMap(DESCRIPTION, blob.getDescription())); + } + + client.putObject(builder.build()); + } + } + + @Override + public void delete(String office, String id) { + String name = buildName(office, id); + try { + client.removeObject(RemoveObjectArgs.builder() + .bucket(requiredBucket()) + .object(name) + .build()); + } catch (ServerException | XmlParserException | ErrorResponseException | InsufficientDataException | + IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidResponseException | + InternalException e) { + throw new RuntimeException(e); + } + } + + private String findFirstKeyById(String id) { + String targetSuffix = "/" + normalizeId(id).toUpperCase(Locale.ROOT); + + ListObjectsArgs args = ListObjectsArgs.builder() + .bucket(requiredBucket()) + .recursive(true) + .build(); + for (Result res : client.listObjects(args)) { + try { + Item item = res.get(); + String name = item.objectName(); + if (name.toUpperCase(Locale.ROOT).endsWith(targetSuffix)) { + return name; + } + } catch (ErrorResponseException | InsufficientDataException | XmlParserException | ServerException | + NoSuchAlgorithmException | IOException | InvalidResponseException | InvalidKeyException | + InternalException e) { + throw new RuntimeException(e); + } + + } + return null; + } + + private static String officeFromKey(String key) { + int slash = key.indexOf('/'); + return (slash > 0) ? key.substring(0, slash) : null; + } + + private static String idFromKey(String key) { + int slash = key.indexOf('/'); + return (slash >= 0 && slash < key.length() - 1) ? key.substring(slash + 1) : key; + } + + private String requiredBucket() { + String bucket = config.bucket(); + if (bucket == null || bucket.isEmpty()) { + throw new IllegalStateException("Object storage bucket is not configured (blob.store.bucket)"); + } + return bucket; + } + + private static String buildName(String office, String id) { + String off = office == null ? "" : office.toUpperCase(Locale.ROOT); + String nid = normalizeId(id).toUpperCase(Locale.ROOT); + String fullKey = off + "/" + nid; + if (fullKey.length() > MAX_KEY_LENGTH) { + throw new FieldLengthExceededException("Key", fullKey.length(), MAX_KEY_LENGTH, null, true); + } + return fullKey; + } + + private static String normalizeId(String id) { + if (id == null) return ""; + + if(id.length() > ID_LENGTH_LIMIT){ + throw new FieldLengthExceededException("ID", id.length(), ID_LENGTH_LIMIT, null, true); + } + // Replace spaces with underscore; leave common safe chars; percent-encode others + StringBuilder sb = new StringBuilder(); + for (char c : id.toCharArray()) { + if (Character.isLetterOrDigit(c) || c == '.' || c == '_' || c == '-' ) { + sb.append(c); + } else if (c == ' ') { + sb.append('_'); + } else if (c == '/') { + // keep slash because controller may pass IDs containing '/'; since we prefix with OFFICE/, this would nest more levels + sb.append('/'); + } else { + String hex = Integer.toHexString(c).toUpperCase(Locale.ROOT); + if (hex.length() == 1) hex = "0" + hex; + sb.append('%').append(hex); + } + } + return sb.toString(); + } + + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageConfig.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageConfig.java new file mode 100644 index 0000000000..442c13514d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ObjectStorageConfig.java @@ -0,0 +1,64 @@ +package cwms.cda.data.dao; + +import java.util.Optional; + +public class ObjectStorageConfig { + + private final String bucket; + + private final String endpoint; + + private final String accessKey; + private final String secretKey; + + public ObjectStorageConfig(String bucket, String endpoint, + String accessKey, String secretKey) { + + this.bucket = bucket; + + this.endpoint = endpoint; + + this.accessKey = accessKey; + this.secretKey = secretKey; + } + + public static ObjectStorageConfig fromSystem() { + + String bucket = get("blob.store.bucket").orElse(null); + + String endpoint = get("blob.store.endpoint").orElse(null); + + String accessKey = get("blob.store.accessKey").orElse(null); + String secretKey = get("blob.store.secretKey").orElse(null); + return new ObjectStorageConfig(bucket, endpoint, accessKey, secretKey); + } + + private static Optional get(String key) { + String sys = System.getProperty(key); + if (sys != null && !sys.isEmpty()) return Optional.of(sys); + String env = System.getenv(toEnvKey(key)); + if (env != null && !env.isEmpty()) return Optional.of(env); + return Optional.empty(); + } + + private static String toEnvKey(String key) { + return key.toUpperCase().replace('.', '_'); + } + + + public String bucket() { + return bucket; + } + + public String endpoint() { + return endpoint; + } + + public String accessKey() { + return accessKey; + } + + public String secretKey() { + return secretKey; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/ParameterDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/ParameterDao.java index d68550c87d..f91a487468 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/ParameterDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/ParameterDao.java @@ -1,21 +1,39 @@ package cwms.cda.data.dao; import cwms.cda.data.dto.Parameter; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.data.dto.ParameterLegacy; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import cwms.cda.formatters.FormattingException; +import mil.army.usace.hec.metadata.UnitUtil; import org.jooq.DSLContext; import org.jooq.Record; import usace.cwms.db.jooq.codegen.packages.CWMS_CAT_PACKAGE; +import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; +import cwms.cda.formatters.json.JsonV2; + +import static java.util.stream.Collectors.toList; public class ParameterDao extends JooqDao { + private static final String LEGACY_JSON_FIELD_NAME = "parameters"; + public ParameterDao(DSLContext dsl) { super(dsl); } public String getParameters(String format) { - return CWMS_CAT_PACKAGE.call_RETRIEVE_PARAMETERS_F(dsl.configuration(), format); + String retVal = CWMS_CAT_PACKAGE.call_RETRIEVE_PARAMETERS_F(dsl.configuration(), format); + if (Formats.JSON_LEGACY.equals(format)) { + retVal = fixDefaultUnits(format, retVal); + } + return retVal; } public List getParametersV2(String office) @@ -23,7 +41,7 @@ public List getParametersV2(String office) return CWMS_CAT_PACKAGE.call_CAT_PARAMETER(dsl.configuration(), office) .stream() .map(this::buildParameter) - .collect(Collectors.toList()); + .collect(toList()); } private Parameter buildParameter(Record record) @@ -38,4 +56,36 @@ private Parameter buildParameter(Record record) String unitDesc = record.get("UNIT_DESCRIPTION", String.class); return new Parameter(param, baseParam, subParam, subParamDesc, dbOfficeId, dbUnitId, unitLongName, unitDesc); } + + private String fixDefaultUnits(String format, String retVal) { + try { + ObjectMapper mapper = JsonV2.buildObjectMapper(); + JsonNode root = mapper.readTree(retVal); + JsonNode wrapper = root.path(LEGACY_JSON_FIELD_NAME); + JsonNode paramsNode = wrapper.path(LEGACY_JSON_FIELD_NAME); + ContentType contentType = new ContentType(format); + List params = Formats.parseContentList(contentType, paramsNode.toString(), ParameterLegacy.class); + params = params.stream() + .map(this::fixDefaultUnits) + .collect(toList()); + ArrayNode newArray = mapper.valueToTree(params); + ((ObjectNode) wrapper).set(LEGACY_JSON_FIELD_NAME, newArray); + retVal = mapper.writeValueAsString(root); + } catch (IOException e) { + throw new FormattingException("Error processing legacy JSON: " + e.getMessage(), e); + } + return retVal; + } + + private ParameterLegacy fixDefaultUnits(ParameterLegacy parameterLegacy) { + var param = mil.army.usace.hec.metadata.Parameter.getParameterForUnitsString(parameterLegacy.getDefaultSiUnit()); + String siUnits = param.getUnitsStringForSystem(UnitUtil.SI_ID); + String enUnits = param.getUnitsStringForSystem(UnitUtil.ENGLISH_ID); + parameterLegacy = new ParameterLegacy.Builder() + .from(parameterLegacy) + .withDefaultSiUnit(siUnits) + .withDefaultEnglishUnit(enUnits) + .build(); + return parameterLegacy; + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java index ca22b869c2..1b45b85648 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RateDao.java @@ -26,6 +26,7 @@ import static java.util.stream.Collectors.toList; +import cwms.cda.api.errors.NoDataRateException; import cwms.cda.api.errors.RateException; import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.TimeSeries; @@ -44,6 +45,7 @@ import org.jooq.ConnectionCallable; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; +import org.jspecify.annotations.Nullable; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; import usace.cwms.db.jooq.codegen.udt.records.DATE_TABLE_TYPE; import usace.cwms.db.jooq.codegen.udt.records.DOUBLE_TAB_T; @@ -72,7 +74,7 @@ public RatedOutput rate(String officeId, String ratingId, RateInputValues input) STR_TAB_T unitsTab = new STR_TAB_T(input.getInputUnits()); unitsTab.add(input.getOutputUnit()); return CWMS_RATING_PACKAGE.call_RATE(context.configuration(), ratingId, - inputValues, unitsTab, formatBool(input.getRound()), ratingDates, null, "UTC", officeId); + inputValues, unitsTab, formatBool(input.getRound()), ratingDates, null, "UTC", officeId); }); return new RatedOutputValues(CwmsId.buildCwmsId(officeId, ratingId), outputValues, input.getOutputUnit()); } @@ -172,15 +174,31 @@ static RuntimeException handleRateDbError(DataAccessException ex) { if (cause instanceof SQLException) { int errorCode = ((SQLException) cause).getErrorCode(); if (errorCode == 20019 || errorCode == 20998) { - String localizedMessage = cause.getLocalizedMessage(); - String[] parts = localizedMessage.split("\n"); - String message = parts[0]; - int index = message.indexOf(":"); - if (index >= 0) { - retval = new RateException(message.substring(index + 1), (SQLException) cause); + String message = getMessage(cause); + if( message != null ) { + retval = new RateException(message, (SQLException) cause); + } + } else if (errorCode == 1403){ + String message = getMessage(cause); + if( message != null ) { + // firstMessage may be like "no data found" + // or "Table rating has no rating points" + retval = new NoDataRateException(message, (SQLException) cause); } } } return retval; } + + private static @Nullable String getMessage(Throwable cause) { + String firstMessage = null; + String localizedMessage = cause.getLocalizedMessage(); + String[] parts = localizedMessage.split("\n"); + String message = parts[0]; + int index = message.indexOf(":"); + if (index >= 0) { + firstMessage = message.substring(index + 1); + } + return firstMessage; + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java index 9e047e769a..c2729ac3d9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingAdapter.java @@ -104,10 +104,14 @@ private static TransitionalRating toTransitional( withAbstractFields(builder, rating); String[] evaluationStrings = rating.getEvaluationStrings(); - builder.withEvaluations(Arrays.asList(evaluationStrings)); + if(evaluationStrings != null) { + builder.withEvaluations(Arrays.asList(evaluationStrings)); + } String[] conditionStrings = rating.getConditionStrings(); - builder.withConditions(Arrays.asList(conditionStrings)); + if(conditionStrings != null) { + builder.withConditions(Arrays.asList(conditionStrings)); + } List ratingSpecIds = new ArrayList<>(); SourceRating[] sourceRatings = rating.getSourceRatings(); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java index e69a0944ce..8876231cea 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingDao.java @@ -24,6 +24,7 @@ package cwms.cda.data.dao; +import cwms.cda.formatters.FormattingException; import hec.data.RatingException; import hec.data.cwmsRating.RatingSet; import java.io.IOException; @@ -33,9 +34,9 @@ public interface RatingDao { - Pattern officeMatcher = Pattern.compile(".*office-id=\"(.*?)\""); + Pattern officeMatcher = Pattern.compile(".*office-id(?:=\"([^\"]+)\"|>([^<]+))"); - void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException; + void create(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException; RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, String specificationId, Instant start, Instant end) throws IOException, RatingException; @@ -46,17 +47,25 @@ String retrieveRatings(String format, String names, String unit, String datum, S String start, String end, String timezone); - void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException; + void store(String ratingSet, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException; void delete(String officeId, String specificationId, Instant start, Instant end); + default void create(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException { + create(ratingSet, replaceBaseCurve, null); + } + + default void store(String ratingSet, boolean replaceBaseCurve) throws IOException, RatingException { + store(ratingSet, replaceBaseCurve, null); + } + static String extractOfficeFromXml(String xml) { Matcher officeMatch = officeMatcher.matcher(xml); if (officeMatch.find()) { - return officeMatch.group(1); + return officeMatch.group(1) != null ? officeMatch.group(1) : officeMatch.group(2); } else { - throw new RuntimeException("Unable to determine office for data set"); + throw new FormattingException("Unable to find office-id element within the XML data set."); } } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingMetadataDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingMetadataDao.java index 25c9cb4357..fe4a62236e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingMetadataDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingMetadataDao.java @@ -155,7 +155,7 @@ public Set getRatingIds(String office, String templateIdMask, int offset Condition condition = specView.ALIASED_ITEM.isNull(); if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); + condition = condition.and(specView.OFFICE_ID.eq(office.toUpperCase())); } if (templateIdMask != null) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java index 8a7b60a102..48309a6220 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSetDao.java @@ -27,6 +27,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.data.dto.rating.RatingSpec; import hec.data.RatingException; import hec.data.cwmsRating.RatingSet; import java.io.IOException; @@ -34,10 +36,14 @@ import java.sql.Timestamp; import java.time.Instant; import java.util.List; + import mil.army.usace.hec.cwms.rating.io.jdbc.ConnectionProvider; import mil.army.usace.hec.cwms.rating.io.jdbc.RatingJdbcFactory; +import org.jetbrains.annotations.Nullable; +import org.jooq.ConnectionRunnable; import org.jooq.DSLContext; import org.jooq.exception.DataAccessException; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; @@ -48,25 +54,8 @@ public RatingSetDao(DSLContext dsl) { } @Override - public void create(String ratingSetXml, boolean replaceBaseCurve) throws IOException, RatingException { - try { - connection(dsl, c -> { - // can't exist if we are creating, if it exists use store - String office = extractOfficeId(ratingSetXml); - DSLContext context = getDslContext(c, office); - String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), - ratingSetXml, "T", replaceBaseCurve ? "T" : "F"); - if (errs != null && !errs.isEmpty()) { - throw new DataAccessException("Failed to create Rating", new RatingException(errs)); - } - }); - } catch (DataAccessException ex) { - Throwable cause = ex.getCause(); - if (cause instanceof RatingException) { - throw (RatingException) cause; - } - throw new IOException("Failed to create Rating", ex); - } + public void create(String ratingSetXml, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException { + connection(dsl, connection -> storeWithDefaultDatum(ratingSetXml, replaceBaseCurve, true, vd, connection)); } private static String extractOfficeId(String ratingSet) throws JsonProcessingException { @@ -81,6 +70,18 @@ private static String extractOfficeId(String ratingSet) throws JsonProcessingExc return office; } + private static String extractLocationId(String ratingSet) throws JsonProcessingException { + XmlMapper xmlMapper = new XmlMapper(); + JsonNode node = xmlMapper.readTree(ratingSet); + List values = node.findValues("location-id"); + String location = ""; + if (!values.isEmpty()) { + //Getting the last instance since the order is template, spec, rating + location = values.get(values.size() - 1).textValue(); + } + return location; + } + @Override public String retrieveLatestXML(String officeId, String specificationId) { return connectionResult(dsl, c -> { @@ -116,9 +117,11 @@ public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, RatingSet.DatabaseLoadMethod finalMethod = method; - connection(dsl, c -> retval[0] = - RatingJdbcFactory.ratingSet(finalMethod, new RatingConnectionProvider(c), officeId, - specificationId, start, end, false)); + connection(dsl, c -> { + setOffice(c, officeId); + retval[0] = RatingJdbcFactory.ratingSet(finalMethod, new RatingConnectionProvider(c), officeId, + specificationId, start, end, false); + }); } catch (DataAccessException ex) { @@ -137,18 +140,20 @@ public RatingSet retrieve(RatingSet.DatabaseLoadMethod method, String officeId, // store/update @Override - public void store(String ratingSetXml, boolean replaceBaseCurve) throws IOException, RatingException { + public void store(String ratingSetXml, boolean replaceBaseCurve, VerticalDatum vd) throws IOException, RatingException { + connection(dsl, connection -> storeWithDefaultDatum(ratingSetXml, replaceBaseCurve, false, vd, connection)); + } + + private static void storeRatingSetXml(String ratingSetXml, boolean replaceBaseCurve, boolean failIfExists, Connection c) throws RatingException, IOException { try { - connection(dsl, c -> { - String office = extractOfficeId(ratingSetXml); - DSLContext context = getDslContext(c, office); - String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), - ratingSetXml, "F", replaceBaseCurve ? "T" : "F"); - if (errs != null && !errs.isEmpty()) - { - throw new DataAccessException("Failed to store Rating", new RatingException(errs)); - } - }); + String office = extractOfficeId(ratingSetXml); + DSLContext context = getDslContext(c, office); + String errs = CWMS_RATING_PACKAGE.call_STORE_RATINGS_XML__5(context.configuration(), + ratingSetXml, formatBool(failIfExists), formatBool(replaceBaseCurve)); + if (errs != null && !errs.isEmpty()) + { + throw new DataAccessException("Failed to store Rating", new RatingException(errs)); + } } catch (DataAccessException ex) { Throwable cause = ex.getCause(); if (cause instanceof RatingException) { @@ -158,6 +163,52 @@ public void store(String ratingSetXml, boolean replaceBaseCurve) throws IOExcept } } + private void storeWithDefaultDatum(String ratingSetXml, boolean replaceBaseCurve, boolean failIfExists, + VerticalDatum vd, Connection connection) throws Throwable { + String office = extractOfficeId(ratingSetXml); + String locationId = extractLocationId(ratingSetXml); + DSLContext dslContext = getDslContext(connection, office); + if(vd != null) { + withLocalAndDefaultDatum(locationId, office, vd, dslContext, c -> storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, c)); + } + else { + storeRatingSetXml(ratingSetXml, replaceBaseCurve, failIfExists, connection); + } + + } + + protected void withLocalAndDefaultDatum(String locationId, String officeId, @Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { + boolean localDatumAdded = false; + try { + //if converting to NAVD88 or NGVD29, we need to set the local datum to the native datum temporarily or the conversion will fail in the db + if(targetDatum == VerticalDatum.NAVD88 || targetDatum == VerticalDatum.NGVD29) { + String vertDatum = CWMS_LOC_PACKAGE.call_GET_VERTICAL_DATUM_INFO_F__2(dslContext.configuration(), locationId, "m", officeId); + if(vertDatum != null) + { + XmlMapper xmlMapper = new XmlMapper(); + VerticalDatumInfo vdi = xmlMapper.readValue(vertDatum, VerticalDatumInfo.class); + String nativeDatum = vdi.getNativeDatum(); + // Only set local datum temporarily if native datum is NAVD88 or NGVD29 to allow conversion + // If native datum is unknown for some reason then just set to the target datum since there is no conversion needed anyways + if(nativeDatum == null || nativeDatum.isBlank() || "UNKNOWN".equalsIgnoreCase(nativeDatum)) { + CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, targetDatum.toString(), "T", officeId); + localDatumAdded = true; + } else if(VerticalDatum.NAVD88 == VerticalDatum.getVerticalDatum(nativeDatum) || VerticalDatum.NGVD29 == VerticalDatum.getVerticalDatum(nativeDatum)) { + CWMS_LOC_PACKAGE.call_SET_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, nativeDatum, "T", officeId); + localDatumAdded = true; + } + } + } + withDefaultDatum(targetDatum, dslContext, cr); + } catch (IOException e) { + throw new DataAccessException("Failed to parse vertical datum info for location " + locationId, e); + } finally { + if(localDatumAdded) { + CWMS_LOC_PACKAGE.call_DELETE_LOCAL_VERT_DATUM_NAME__2(dslContext.configuration(), locationId, officeId); + } + } + } + @Override public void delete(String officeId, String specificationId, Instant start, Instant end) { Timestamp startDate = new Timestamp(start.toEpochMilli()); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java index 16a345da86..babf39f73a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecDao.java @@ -27,13 +27,16 @@ import static com.google.common.flogger.LazyArgs.lazy; import static cwms.cda.data.dto.rating.RatingSpec.Builder.buildIndependentRoundingSpecs; +import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; +import com.fasterxml.jackson.core.JsonProcessingException; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.data.dto.rating.RatingEffectiveDatesMap; import cwms.cda.data.dto.rating.RatingSpec; import cwms.cda.data.dto.rating.RatingSpecEffectiveDates; import cwms.cda.data.dto.rating.RatingSpecs; + import java.sql.CallableStatement; import java.sql.Connection; import java.sql.ResultSet; @@ -46,23 +49,26 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.NavigableSet; +import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; + import com.google.common.flogger.FluentLogger; + import java.util.stream.Collectors; import java.util.stream.Stream; import javax.sql.rowset.CachedRowSet; import javax.sql.rowset.RowSetProvider; + +import cwms.cda.formatters.FormattingException; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; @@ -108,69 +114,8 @@ public RatingSpecDao(DSLContext dsl) { super(dsl); } - public Collection retrieveRatingSpecs(String office, String specIdMask) { - - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; - AV_RATING ratView = AV_RATING.AV_RATING; - - // We don't want to also check AV_RATING_SPEC.ALIASED_ITEM b/c we - // don't care whether the specs returned are an alias or not. - // We do want to exclude the aliased ratings b/c we only want one - // copy of each matching rating. - - Condition condition = ratView.ALIASED_ITEM.isNull(); - - if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); - } - - if (specIdMask != null) { - Condition likeRegex = JooqDao.caseInsensitiveLikeRegex(specView.RATING_ID, specIdMask); - condition = condition.and(likeRegex); - } - - ResultQuery query = dsl.select(specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.TEMPLATE_ID, - specView.LOCATION_ID, specView.VERSION, specView.SOURCE_AGENCY, - specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, - specView.AUTO_MIGRATE_EXT_FLAG, specView.IND_ROUNDING_SPECS, - specView.DEP_ROUNDING_SPEC, specView.DATE_METHODS, specView.DESCRIPTION, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .fetchSize(DEFAULT_FETCH_SIZE); - - logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { - RatingSpec template = buildRatingSpec(rec); - - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } - - return map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - - public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String office, - String specIdMask) { + @NotNull + public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String office, String specIdMask) { Integer total = null; int offset = 0; @@ -190,145 +135,119 @@ public RatingSpecs retrieveRatingSpecs(String cursor, int pageSize, String offic } } - Set retval = getRatingSpecs(office, specIdMask, offset, pageSize); - - RatingSpecs.Builder builder = new RatingSpecs.Builder(offset, pageSize, total); - builder.specs(new ArrayList<>(retval)); - return builder.build(); - } - - @NotNull - public Set getRatingSpecs(String office, String specIdMask, int firstRow, - int pageSize) { - Set retVal; - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; AV_RATING ratView = AV_RATING.AV_RATING; - // We don't want to also check AV_RATING_SPEC.ALIASED_ITEM b/c we - // don't care whether the specs returned are an alias or not. - // We do want to exclude the aliased ratings b/c we only want one - // copy of each matching rating. - Condition condition = ratView.ALIASED_ITEM.isNull(); + // Conditions that define WHICH SPECS match + Condition specCondition = specView.TEMPLATE_ID.notLike("%Stage-Offset%") + .and(specView.TEMPLATE_ID.notLike("%Stage-Shift%")) + .and(specView.ALIASED_ITEM.isNull()); if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); + specCondition = specCondition.and(specView.OFFICE_ID.eq(office.toUpperCase())); } if (specIdMask != null) { Condition maskRegex = JooqDao.caseInsensitiveLikeRegex(specView.RATING_ID, specIdMask); - condition = condition.and(maskRegex); + specCondition = specCondition.and(maskRegex); } - ResultQuery query = dsl.select(specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.DATE_METHODS, - specView.TEMPLATE_ID, specView.LOCATION_ID, specView.VERSION, - specView.SOURCE_AGENCY, specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, - specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, - specView.DESCRIPTION, specView.ALIASED_ITEM, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .orderBy(specView.OFFICE_ID, specView.TEMPLATE_ID, ratView.RATING_ID, - ratView.EFFECTIVE_DATE) - .limit(pageSize) - .offset(firstRow); - - logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { - RatingSpec template = buildRatingSpec(rec); - - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } - - retVal = map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - return retVal; - } - - - public Optional retrieveRatingSpec(String office, String specId) { - Set retVal; - - AV_RATING_SPEC specView = AV_RATING_SPEC.AV_RATING_SPEC; - AV_RATING ratView = AV_RATING.AV_RATING; - - Condition condition = ratView.ALIASED_ITEM.isNull(); - - if (specId != null) { - condition = condition.and(specView.RATING_ID.eq(specId)); + if (total == null) { + total = dsl.fetchCount(specView, specCondition); } - if (office != null) { - condition = condition.and(specView.OFFICE_ID.eq(office)); - } + var specPage = dsl + .select( + specView.RATING_SPEC_CODE, + specView.OFFICE_ID, specView.RATING_ID, specView.DATE_METHODS, + specView.TEMPLATE_ID, specView.LOCATION_ID, specView.VERSION, + specView.SOURCE_AGENCY, specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, + specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, + specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, + specView.DESCRIPTION, specView.ALIASED_ITEM + ) + .from(specView) + .where(specCondition) + .orderBy(specView.OFFICE_ID, specView.RATING_ID) + .limit(pageSize) + .offset(offset) + .asTable("spec_page"); + + Field spSpecCode = specPage.field(specView.RATING_SPEC_CODE); + Field spOfficeId = specPage.field(specView.OFFICE_ID); + Field spRatingId = specPage.field(specView.RATING_ID); + + Field> effectiveDates = + DSL.multiset( + dsl.select(ratView.EFFECTIVE_DATE) + .from(ratView) + .where(ratView.RATING_SPEC_CODE.eq(spSpecCode)) + .and(ratView.ALIASED_ITEM.isNull()) + .orderBy(ratView.EFFECTIVE_DATE) + ) + .convertFrom(r -> + r.getValues(ratView.EFFECTIVE_DATE).stream() + .map(RatingSpecDao::toZdt) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ) + .as("effective_dates"); ResultQuery query = dsl.select( - specView.RATING_SPEC_CODE, - specView.OFFICE_ID, specView.RATING_ID, specView.TEMPLATE_ID, - specView.LOCATION_ID, specView.VERSION, specView.SOURCE_AGENCY, - specView.ACTIVE_FLAG, specView.AUTO_UPDATE_FLAG, - specView.AUTO_ACTIVATE_FLAG, specView.AUTO_MIGRATE_EXT_FLAG, - specView.IND_ROUNDING_SPECS, specView.DEP_ROUNDING_SPEC, - specView.DATE_METHODS, specView.DESCRIPTION, - ratView.RATING_SPEC_CODE, ratView.EFFECTIVE_DATE - ) - .from(specView) - .leftOuterJoin(ratView) - .on(specView.RATING_SPEC_CODE.eq(ratView.RATING_SPEC_CODE)) - .where(condition) - .orderBy(specView.OFFICE_ID, specView.RATING_ID, ratView.EFFECTIVE_DATE) - .fetchSize(DEFAULT_FETCH_SIZE); + spSpecCode, + spOfficeId, + spRatingId, + specPage.field(specView.DATE_METHODS), + specPage.field(specView.TEMPLATE_ID), + specPage.field(specView.LOCATION_ID), + specPage.field(specView.VERSION), + specPage.field(specView.SOURCE_AGENCY), + specPage.field(specView.ACTIVE_FLAG), + specPage.field(specView.AUTO_UPDATE_FLAG), + specPage.field(specView.AUTO_ACTIVATE_FLAG), + specPage.field(specView.AUTO_MIGRATE_EXT_FLAG), + specPage.field(specView.IND_ROUNDING_SPECS), + specPage.field(specView.DEP_ROUNDING_SPEC), + specPage.field(specView.DESCRIPTION), + specPage.field(specView.ALIASED_ITEM), + effectiveDates + ) + .from(specPage) + .orderBy(spOfficeId, spRatingId) + .fetchSize(DEFAULT_FETCH_SIZE); logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - Map> map = new LinkedHashMap<>(); - try (Stream stream = query.fetchStream()) { - stream.forEach(rec -> { + List specs = query.fetch() + .stream() + .map(rec -> { RatingSpec template = buildRatingSpec(rec); + List dates = rec.get(effectiveDates); + return new RatingSpec.Builder() + .fromRatingSpec(template) + .withEffectiveDates(dates == null ? List.of() : dates) + .build(); + }) + .collect(toList()); - Timestamp effectiveDate = rec.get(ratView.EFFECTIVE_DATE); - ZonedDateTime effective = toZdt(effectiveDate); - - List list = map.computeIfAbsent(template, k -> new ArrayList<>()); - if (effective != null) { - list.add(effective); - } - }); - } + RatingSpecs.Builder builder = new RatingSpecs.Builder(offset, pageSize, total); + builder.withSpecs(specs); + return builder.build(); + } - retVal = map.entrySet().stream() - .map(entry -> new RatingSpec.Builder() - .fromRatingSpec(entry.getKey()) - .withEffectiveDates(entry.getValue()) - .build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - // There should only be one key in the map - if (retVal.size() > 1) { - throw new IllegalStateException("More than one rating spec found for id: " + specId); + public Optional retrieveRatingSpec(String office, String specId) { + RatingSpecs ratingSpecs = retrieveRatingSpecs(null, 1, office, specId); + List specs = ratingSpecs.getSpecs(); + if(specs.size() > 1) { + throw new IllegalStateException("More than one rating spec found for specId: " + specId); + } else if(specs.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(specs.get(0)); + } } - return retVal.stream().findFirst(); - } - public static ZonedDateTime toZdt(final Timestamp time) { if (time != null) { return ZonedDateTime.ofInstant(time.toInstant(), ZoneId.of("UTC")); @@ -388,7 +307,7 @@ public static RatingSpec buildRatingSpec(Record rec) { public void delete(String office, DeleteMethod deleteMethod, String ratingSpecId) { String deleteAction; - switch(deleteMethod) { + switch (deleteMethod) { case DELETE_ALL: deleteAction = DeleteRule.DELETE_ALL.getRule(); break; @@ -400,24 +319,41 @@ public void delete(String office, DeleteMethod deleteMethod, String ratingSpecId break; default: throw new IllegalArgumentException("Delete Method provided does not match accepted rule constants: " - + deleteMethod); + + deleteMethod); } dsl.connection(c -> - CWMS_RATING_PACKAGE.call_DELETE_SPECS( - getDslContext(c,office).configuration(), - ratingSpecId, - deleteAction, - office) + CWMS_RATING_PACKAGE.call_DELETE_SPECS( + getDslContext(c, office).configuration(), + ratingSpecId, + deleteAction, + office) ); } + public void create(RatingSpec spec, boolean failIfExists) { + String xml = null; + try { + xml = RatingSpecXmlUtils.toPlSqlXml(spec); + create(xml, failIfExists); + } catch (JsonProcessingException ex) { + String msg = spec != null ? + "Error rendering '" + spec + "' to XML" + : + "Null element passed to formatter"; + logger.atWarning().withCause(ex).log(msg); + throw new FormattingException(msg, ex); + } + } + + // In my tests this method wouldn't fail if the input was + // mostly right, it just wouldn't create anything. public void create(String xml, boolean failIfExists) { final String office = RatingDao.extractOfficeFromXml(xml); dsl.connection(c -> - CWMS_RATING_PACKAGE.call_STORE_SPECS__3( - getDslContext(c,office).configuration(), - xml, - formatBool(failIfExists)) + CWMS_RATING_PACKAGE.call_STORE_SPECS__3( + getDslContext(c, office).configuration(), + xml, + formatBool(failIfExists)) ); } @@ -432,21 +368,21 @@ public RatingEffectiveDatesMap retrieveSpecEffectiveDates(String officeIdMask, S //office->spec->dates NavigableMap>> specDateMap = new TreeMap<>(); //instantiate empty Instant list for each office/spec combination so that specs with no effective dates are included in the final result - for(Map.Entry> entry : officeToRatingIdsNoAliasesMap.entrySet()) { + for (Map.Entry> entry : officeToRatingIdsNoAliasesMap.entrySet()) { String officeId = entry.getKey(); List specIds = entry.getValue(); NavigableMap> specMap = specDateMap.computeIfAbsent(officeId, k -> new TreeMap<>()); - for(String specId : specIds) { + for (String specId : specIds) { specMap.put(specId, new TreeSet<>()); } } - try(ResultSet rs = catRatings(conn, officeIdMask, specIdMask, begin, end)) { + try (ResultSet rs = catRatings(conn, officeIdMask, specIdMask, begin, end)) { checkMetaData(rs.getMetaData(), RATINGS_COLUMN_LIST, "Ratings"); - while(rs.next()) { + while (rs.next()) { String officeId = rs.getString(OFFICE_ID); String specId = rs.getString(SPECIFICATION_ID); List ratingIdsNoAliases = officeToRatingIdsNoAliasesMap.get(officeId); - if(ratingIdsNoAliases != null && !ratingIdsNoAliases.contains(specId)) { // skip aliased specs based on queried list of rating ids not including aliases + if (ratingIdsNoAliases != null && !ratingIdsNoAliases.contains(specId)) { // skip aliased specs based on queried list of rating ids not including aliases continue; } Timestamp timestamp = rs.getTimestamp(EFFECTIVE_DATE, GMT_CALENDAR); @@ -463,11 +399,11 @@ public RatingEffectiveDatesMap retrieveSpecEffectiveDates(String officeIdMask, S //package scoped for unit testing static RatingEffectiveDatesMap buildRatingEffectiveDatesMap(NavigableMap>> specDateMap) { Map> officeToSpecDatesMap = new LinkedHashMap<>(specDateMap.size()); - for(Map.Entry>> entry : specDateMap.entrySet()) { + for (Map.Entry>> entry : specDateMap.entrySet()) { String officeId = entry.getKey(); List specEffectiveDatesForOffice = new ArrayList<>(); NavigableMap> specMap = entry.getValue(); - for(Map.Entry> specEntry : specMap.entrySet()) { + for (Map.Entry> specEntry : specMap.entrySet()) { String specId = specEntry.getKey(); NavigableSet dateList = specEntry.getValue(); if (dateList.isEmpty()) { @@ -495,8 +431,7 @@ private ResultSet catRatings(Connection conn, String officeIdMask, String specId CachedRowSet output = RowSetProvider.newFactory() .createCachedRowSet(); - try (CallableStatement statement = conn.prepareCall("{CALL CWMS_20.CWMS_RATING.CAT_RATINGS(?, ?, ?, ?, ?, ?)}")) - { + try (CallableStatement statement = conn.prepareCall("{CALL CWMS_20.CWMS_RATING.CAT_RATINGS(?, ?, ?, ?, ?, ?)}")) { statement.registerOutParameter(1, Types.REF_CURSOR); statement.setString(2, specIdMask); statement.setTimestamp(3, pEffectiveDateStart, GMT_CALENDAR); @@ -518,7 +453,7 @@ private Map> getRatingIds(String office, String templateIdM Condition condition = DSL.noCondition(); if (office != null && !office.isEmpty() && !office.equals("*")) { - condition = condition.and(specView.OFFICE_ID.eq(office)); + condition = condition.and(specView.OFFICE_ID.eq(office.toUpperCase())); } if (templateIdMask != null) { @@ -527,7 +462,7 @@ private Map> getRatingIds(String office, String templateIdM condition = condition.and(ratingIdLike); } - if(!includeAliases) { + if (!includeAliases) { condition = condition.and(specView.ALIASED_ITEM.isNull()); } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java new file mode 100644 index 0000000000..af2a8514f4 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingSpecXmlUtils.java @@ -0,0 +1,88 @@ +package cwms.cda.data.dao; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dto.rating.IndependentRoundingSpec; +import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.formatters.xml.XMLv2; + +import java.time.ZonedDateTime; +import java.util.List; + + +abstract class RatingSpecPlSqlMixin { + @JacksonXmlProperty(isAttribute = true, localName = "office-id") + abstract String getOfficeId(); + + @JacksonXmlProperty(localName = "rating-spec-id") + abstract String getRatingId(); + + @JacksonXmlElementWrapper(localName = "ind-rounding-specs") + @JacksonXmlProperty(localName = "ind-rounding-spec") + abstract IndependentRoundingSpec[] getIndependentRoundingSpecs(); + + @JacksonXmlProperty(localName = "dep-rounding-spec") + abstract String getDependentRoundingSpec(); + + @JsonIgnore + abstract List getEffectiveDates(); +} + + +@JacksonXmlRootElement(localName = "ratings") +class RatingSpecWrapper { + @JacksonXmlProperty(isAttribute = true, localName = "xmlns:xsi") + final String xsi = "http://www.w3.org/2001/XMLSchema-instance"; + + @JacksonXmlProperty(isAttribute = true, localName = "xsi:noNamespaceSchemaLocation") + final String schemaLocation = "http://www.hec.usace.army.mil/xmlSchema/cwms/Ratings.xsd"; + + @JacksonXmlProperty(localName = "rating-spec") + private final RatingSpec ratingSpec; + + public RatingSpecWrapper(RatingSpec ratingSpec) { + this.ratingSpec = ratingSpec; + } + + public RatingSpec getRatingSpec() { + return ratingSpec; + } +} + + +class RatingSpecXmlUtils { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final XmlMapper mapper = buildMapper(); + + private static XmlMapper buildMapper() { + XmlMapper m = XMLv2.buildXmlMapper(); + + // We don't want to globally mess with how RatingSpec is serialized, just when + // it comes thru this class. + m.addMixIn(RatingSpec.class, RatingSpecPlSqlMixin.class); + m.enable(SerializationFeature.INDENT_OUTPUT); + m.configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION, true); + return m; + } + + + /** + * The pl/sql create call is expecting a very particular format of xml. + * CDA publishes a RatingSpec object based on a DTO class. The CDA RatingSpec class does + * not quite match what the pl/sql wants. This method is meant to take a CDA RatingSpec + * as input and coax it into the format that the pl/sql wants. + * + * @param spec The CDA RatingSpec object. + * @return xml String in the format expected by the pl/sql create call. + */ + public static String toPlSqlXml(RatingSpec spec) throws JsonProcessingException { + return mapper.writeValueAsString(new RatingSpecWrapper(spec)); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingTemplateDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingTemplateDao.java index cc8bec9c1c..c386803769 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingTemplateDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingTemplateDao.java @@ -47,7 +47,6 @@ import org.jooq.Record; import org.jooq.ResultQuery; import org.jooq.SelectForUpdateStep; -import org.jooq.TableField; import org.jooq.conf.ParamType; import usace.cwms.db.jooq.codegen.packages.CWMS_RATING_PACKAGE; @@ -69,7 +68,7 @@ public Set retrieveRatingTemplates(String office, String templat AV_RATING_TEMPLATE tempView = AV_RATING_TEMPLATE.AV_RATING_TEMPLATE; if (office != null) { - condition = condition.and(tempView.OFFICE_ID.eq(office)); + condition = condition.and(tempView.OFFICE_ID.eq(office.toUpperCase())); } if (templateIdMask != null) { @@ -119,7 +118,7 @@ public Optional retrieveRatingTemplate(String office, String tem .and(specView.ALIASED_ITEM.isNull()); if (office != null) { - condition = condition.and(tempView.OFFICE_ID.eq(office)); + condition = condition.and(tempView.OFFICE_ID.eq(office.toUpperCase())); } ResultQuery query = dsl.select( @@ -253,7 +252,7 @@ private Set getRatingTemplates(String office, String templateIdM Condition condition = specView.ALIASED_ITEM.isNull(); if (office != null) { - condition = condition.and(tempView.OFFICE_ID.eq(office)); + condition = condition.and(tempView.OFFICE_ID.eq(office.toUpperCase())); } if (templateIdMask != null) { diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java new file mode 100644 index 0000000000..1b60bbdf13 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/RatingsVerticalDatumExtractor.java @@ -0,0 +1,60 @@ +package cwms.cda.data.dao; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import cwms.cda.data.dto.VerticalDatumInfo; + +import java.util.List; +import java.util.Optional; + +public class RatingsVerticalDatumExtractor { + + private RatingsVerticalDatumExtractor() { + throw new AssertionError("Utility class, don't instantiate"); + } + + public static Optional getVerticalDatum(String ratingSet) { + return Optional.ofNullable(ratingSet) + .flatMap(RatingsVerticalDatumExtractor::getVerticalDatumInfo) + .map(VerticalDatumInfo::getNativeDatum) + .filter(s -> !s.isEmpty()) + .map(s -> { + if (s.equalsIgnoreCase(VerticalDatum.OTHER.toString())) { + throw new IllegalArgumentException("Vertical Datum of OTHER is not currently supported."); + } + return VerticalDatum.getVerticalDatum(s); + }); + } + + public static Optional getVerticalDatumInfo(String ratingSet) { + try { + return extractVerticalDatumInfo(ratingSet).map(RatingsVerticalDatumExtractor::deserializeVerticalDatumInfoXml); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse Vertical Datum Info", e); + } + } + + public static VerticalDatumInfo deserializeVerticalDatumInfoXml(String vdiXml) { + XmlMapper xmlMapper = new XmlMapper(); + try { + return xmlMapper.readValue(vdiXml, VerticalDatumInfo.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Failed to parse Vertical Datum Info", e); + } + } + + private static Optional extractVerticalDatumInfo(String ratingSet) throws JsonProcessingException { + XmlMapper xmlMapper = new XmlMapper(); + JsonNode node = xmlMapper.readTree(ratingSet); + List values = node.findValues("vertical-datum-info"); + Optional retVal = Optional.empty(); + if (!values.isEmpty()) { + JsonNode vdiNode = values.get(values.size() - 1); + retVal = Optional.ofNullable(xmlMapper.writer() + .withRootName("vertical-datum-info") + .writeValueAsString(vdiNode)); + } + return retVal; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/StreamConsumer.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/StreamConsumer.java new file mode 100644 index 0000000000..8e337ab413 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/StreamConsumer.java @@ -0,0 +1,10 @@ +package cwms.cda.data.dao; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; + +@FunctionalInterface +public interface StreamConsumer { + void accept(InputStream stream, long inputStreamPosition, String mediaType, long totalLength) throws SQLException, IOException; +} \ No newline at end of file diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesCategoryDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesCategoryDao.java index 20bd27b844..5e1f22345b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesCategoryDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesCategoryDao.java @@ -25,8 +25,11 @@ package cwms.cda.data.dao; import cwms.cda.data.dto.TimeSeriesCategory; +import cwms.cda.data.dto.TimeSeriesGroup; import java.util.List; +import java.util.Objects; import java.util.Optional; +import org.jooq.Configuration; import org.jooq.DSLContext; import org.jooq.Record3; import org.jooq.Select; @@ -46,7 +49,7 @@ public Optional getTimeSeriesCategory(String officeId, Strin Record3 fetchOne = dsl.selectDistinct(view.CAT_DB_OFFICE_ID, view.TS_CATEGORY_ID, view.TS_CATEGORY_DESC) .from(view) - .where(view.CAT_DB_OFFICE_ID.eq(officeId)) + .where(view.CAT_DB_OFFICE_ID.eq(officeId.toUpperCase())) .and(view.TS_CATEGORY_ID.eq(categoryId)) .fetchOne(); @@ -63,7 +66,7 @@ public List getTimeSeriesCategories(String officeId) { Select select; if ( officeId != null && !officeId.isEmpty()) { - select = step.where(table.CAT_DB_OFFICE_ID.eq(officeId)); + select = step.where(table.CAT_DB_OFFICE_ID.eq(officeId.toUpperCase())); }else { select = step; } @@ -76,23 +79,67 @@ public List getTimeSeriesCategories() { } public void delete(String categoryId, boolean cascadeDelete, String office) { + + if(cascadeDelete){ + cascadeDelete(categoryId, office); + } else { + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + CWMS_TS_PACKAGE.call_DELETE_TS_CATEGORY(dslContext.configuration(), categoryId, formatBool(false), office); + }); + } + } + + private void cascadeDelete(String categoryId, String office) { connection(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); - CWMS_TS_PACKAGE.call_DELETE_TS_CATEGORY( - dslContext.configuration(), categoryId, - formatBool(cascadeDelete), office); - }); + if (getDbVersion() > Dao.CWMS_25_07_01) { + // With newer schema it should just work, don't need transaction + Configuration config = dslContext.configuration(); + CWMS_TS_PACKAGE.call_DELETE_TS_CATEGORY(config, categoryId, formatBool(true), office); + } else { + // Before 2/3/2026 DELETE_TS_CATEGORY wasn't removing assignments from groups so we start a transaction and do the deletes + + dslContext.transaction((Configuration trx) -> { + DSLContext context = trx.dsl(); + Configuration config = context.configuration(); + + TimeSeriesGroupDao dao = new TimeSeriesGroupDao(context); + List timeSeriesGroups = dao.getTimeSeriesGroups(null, null, office, false, categoryId, null); + for (TimeSeriesGroup group : timeSeriesGroups) { + dao.delete(categoryId, group.getId(), office, true); + } + + // Before 2/3/2026 DELETE_TS_CATEGORY wasn't removing assignments from groups + CWMS_TS_PACKAGE.call_DELETE_TS_CATEGORY(config, categoryId, formatBool(true), office); + }); + } + + }); } - public void create(TimeSeriesCategory category, boolean failIfExists) { + public void create(TimeSeriesCategory category, boolean failIfExists, boolean ignoreNulls) { String office = category.getOfficeId(); connection(dsl, conn -> { DSLContext dslContext = getDslContext(conn, office); CWMS_TS_PACKAGE.call_STORE_TS_CATEGORY( - dslContext.configuration(), category.getId(), category.getDescription(), - formatBool(failIfExists), "T", office); + dslContext.configuration(), category.getId(), category.getDescription(), + formatBool(failIfExists), formatBool(ignoreNulls), office); + }); + } + + public void update(String oldCategoryId, TimeSeriesCategory category, boolean ignoreNulls) { + String office = category.getOfficeId(); + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + if(!Objects.equals(oldCategoryId, category.getId())) + { + // When the old and new are the same RENAME throws + CWMS_TS_PACKAGE.call_RENAME_TS_CATEGORY(dslContext.configuration(), oldCategoryId, category.getId(), office); + } + CWMS_TS_PACKAGE.call_STORE_TS_CATEGORY(dslContext.configuration(), category.getId(), category.getDescription(), "F", formatBool(ignoreNulls), office); }); } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java index 9230d22e7f..6dabcad233 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesDaoImpl.java @@ -21,7 +21,6 @@ import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.selectDistinct; -import org.jooq.ConnectionRunnable; import usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID; import static org.jooq.impl.DSL.table; import static usace.cwms.db.jooq.codegen.tables.AV_CWMS_TS_ID2.AV_CWMS_TS_ID2; @@ -65,7 +64,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import com.google.common.flogger.FluentLogger; @@ -1040,7 +1038,7 @@ public TsvDqu findMostRecent(String officeId, String tsId, String unit, Condition nestedCondition = view.ALIASED_ITEM.isNull() .and(view.VALUE.isNotNull()) .and(view.CWMS_TS_ID.eq(tsId)) - .and(view.OFFICE_ID.eq(officeId)); + .and(view.OFFICE_ID.eq(officeId.toUpperCase())); if (twoWeeksFromNow != null) { nestedCondition = nestedCondition.and(view.DATE_TIME.lt(twoWeeksFromNow)); @@ -1062,7 +1060,7 @@ public TsvDqu findMostRecent(String officeId, String tsId, String unit, .from(view) .where(view.DATE_TIME.in(maxSelect)) .and(view.CWMS_TS_ID.eq(tsId)) - .and(view.OFFICE_ID.eq(officeId)) + .and(view.OFFICE_ID.eq(officeId.toUpperCase())) .and(view.UNIT_ID.eq(unit)) .and(view.VALUE.isNotNull()) .and(view.ALIASED_ITEM.isNull()) @@ -1133,7 +1131,7 @@ public List findMostRecentsInRange(String office, List tsId // build whereCondition depending on office Condition whereCondition = AV_CWMS_TS_ID2.CWMS_TS_ID.in(tsIds); if (office != null) { - whereCondition = whereCondition.and(AV_CWMS_TS_ID2.DB_OFFICE_ID.eq(office)); + whereCondition = whereCondition.and(AV_CWMS_TS_ID2.DB_OFFICE_ID.eq(office.toUpperCase())); } // create baseIds alias @@ -1285,7 +1283,7 @@ public List findRecentsInRange(String office, String categoryId, St whereCondition = whereCondition.and(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.GROUP_ID.eq(groupId)); } if (office != null) { - whereCondition = whereCondition.and(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.DB_OFFICE_ID.eq(office)); + whereCondition = whereCondition.and(AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN.DB_OFFICE_ID.eq(office.toUpperCase())); } CommonTableExpression baseIds = name("base_ids").as( @@ -1476,36 +1474,6 @@ public void create(TimeSeries input, }); } - // - - /** - * The idea here is that this will check the current default datum, - * possible switch to the specified datum and - * then run the code and - * if the datum was previously switched - * then switch back to the initial datum. - * @param targetDatum The desired ver - * @param dslContext - * @param cr - */ - private void withDefaultDatum(@Nullable VerticalDatum targetDatum, DSLContext dslContext, ConnectionRunnable cr) { - String defaultVertDatum = CWMS_LOC_PACKAGE.call_GET_DEFAULT_VERTICAL_DATUM(dslContext.configuration()); - String targetName = (targetDatum != null) ? targetDatum.toString() : null; - boolean changeDefaultDatum = !Objects.equals(targetDatum, defaultVertDatum); - try { - if (changeDefaultDatum) { - CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), targetName); - } - - connection(dslContext, cr); - }finally{ - if (changeDefaultDatum) { - // If we changed it we should restore. - CWMS_LOC_PACKAGE.call_SET_DEFAULT_VERTICAL_DATUM(dslContext.configuration(), defaultVertDatum); - } - } - } - @Override public void store(TimeSeries timeSeries, Timestamp versionDate) { store(timeSeries, false, StoreRule.REPLACE_ALL, TimeSeriesDaoImpl.OVERRIDE_PROTECTION, null); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java index 45598cd39a..76cade003a 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesGroupDao.java @@ -28,11 +28,15 @@ import static java.util.stream.Collectors.toList; import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dao.timeseriesgroup.DELETE_TS_GROUP_CASCADE; import cwms.cda.data.dto.AssignedTimeSeries; import cwms.cda.data.dto.TimeSeriesCategory; import cwms.cda.data.dto.TimeSeriesGroup; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.Configuration; @@ -45,6 +49,7 @@ import org.jooq.SelectSeekStep4; import org.jooq.conf.ParamType; import org.jooq.impl.DSL; +import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_TS_PACKAGE; import usace.cwms.db.jooq.codegen.tables.AV_TS_CAT_GRP; import usace.cwms.db.jooq.codegen.tables.AV_TS_GRP_ASSGN; @@ -56,6 +61,14 @@ public class TimeSeriesGroupDao extends JooqDao { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public static final String CWMS = "CWMS"; + private enum DeleteTsGroupCascadeMode { + UNKNOWN, + USE_CASCADE_ROUTINE, + USE_UNASSIGN + } + + private static volatile DeleteTsGroupCascadeMode deleteTsGroupCascadeMode = DeleteTsGroupCascadeMode.UNKNOWN; + public TimeSeriesGroupDao(DSLContext dsl) { super(dsl); } @@ -67,10 +80,10 @@ public List getTimeSeriesGroups() { public List getTimeSeriesGroups(String tsOfficeId, String groupOfficeId, String categoryOfficeId) { Condition whereCond = DSL.noCondition(); if (tsOfficeId != null) { - whereCond = AV_TS_CAT_GRP.AV_TS_CAT_GRP.GRP_DB_OFFICE_ID.eq(tsOfficeId); + whereCond = AV_TS_CAT_GRP.AV_TS_CAT_GRP.GRP_DB_OFFICE_ID.eq(tsOfficeId.toUpperCase()); } - return getTimeSeriesGroupsWhere(whereCond, tsOfficeId, groupOfficeId, categoryOfficeId); + return getTimeSeriesGroupsWhere(dsl, whereCond, tsOfficeId, groupOfficeId, categoryOfficeId); } public List getTimeSeriesGroups(String tsOfficeId, String groupOfficeId, String categoryOfficeId, @@ -91,7 +104,7 @@ public List getTimeSeriesGroups(String tsOfficeId, String group } if (includeAssigned) { - return getTimeSeriesGroupsWhere(whereCond, tsOfficeId, groupOfficeId, categoryOfficeId); + return getTimeSeriesGroupsWhere(dsl, whereCond, tsOfficeId, groupOfficeId, categoryOfficeId); } else { return getTimeSeriesGroupsWithoutAssigned(whereCond); } @@ -99,15 +112,55 @@ public List getTimeSeriesGroups(String tsOfficeId, String group } - public List getTimeSeriesGroups(String tsOfficeId, String groupOfficeId, String categoryOfficeId, - String categoryId, String groupId) { - return getTimeSeriesGroupsWhere(buildWhereCondition(categoryId, groupId), tsOfficeId, groupOfficeId, - categoryOfficeId); + public List getTimeSeriesGroups(DSLContext dslContext, String tsOfficeId, String groupOfficeId, String categoryOfficeId, + String categoryId, String groupId) { + return getTimeSeriesGroupsWhere(dslContext, buildWhereCondition(categoryId, groupId), tsOfficeId, + groupOfficeId, categoryOfficeId); + } + + + public TimeSeriesGroup getTimeSeriesGroup(String tsOfficeId, String groupOfficeId, String categoryOfficeId, + String categoryId, String groupId) { + return getTimeSeriesGroup(dsl, tsOfficeId, groupOfficeId, categoryOfficeId, categoryId, groupId); + } + + /** + * + * @param dslContext The context to be used to avoid creating a new connection. + * @param tsOfficeId The office id. + * @param groupOfficeId The group office id. + * @param categoryOfficeId The category office id. + * @param categoryId The category id. + * @param groupId The group id. + * @return retrieved TimeSeriesGroup. + */ + public TimeSeriesGroup getTimeSeriesGroup(DSLContext dslContext, String tsOfficeId, String groupOfficeId, String categoryOfficeId, + String categoryId, String groupId) { + List timeSeriesGroups = getTimeSeriesGroups(dslContext, tsOfficeId, groupOfficeId, categoryOfficeId, categoryId, groupId); + if (timeSeriesGroups != null && !timeSeriesGroups.isEmpty()) { + if (timeSeriesGroups.size() == 1) { + return timeSeriesGroups.get(0); + } else { + throw new IllegalArgumentException(String.format("Multiple TimeSeriesGroups returned from " + + "getTimeSeriesGroups for office:%s category:%s group:%s At most one match was expected.", + tsOfficeId, categoryId, groupId)); + } + } + return null; } + /** + * + * @param dslContext Jooq Context to be used + * @param whereCond Additional whereCondition that will be added to the query. + * @param tsOfficeId If provided, the assigned time series that are retrieved will be restricted to this office. + * @param groupOfficeId If provided, the retrieved groups must be in this office + * @param categoryOfficeId If provided, the retrieve groups must be in categories that are in this office + * @return retrieved TimeSeriesGroups. + */ @NotNull - private List getTimeSeriesGroupsWhere(Condition whereCond, String tsOfficeId, String groupOfficeId, - String categoryOfficeId) { + private List getTimeSeriesGroupsWhere(DSLContext dslContext, Condition whereCond, String tsOfficeId, String groupOfficeId, + String categoryOfficeId) { AV_TS_CAT_GRP catGrp = AV_TS_CAT_GRP.AV_TS_CAT_GRP; AV_TS_GRP_ASSGN grpAssgn = AV_TS_GRP_ASSGN.AV_TS_GRP_ASSGN; @@ -122,11 +175,11 @@ private List getTimeSeriesGroupsWhere(Condition whereCond, Stri Condition joinCond = catGrp.TS_CATEGORY_ID.eq(grpAssgn.CATEGORY_ID) .and(catGrp.TS_GROUP_ID.eq(grpAssgn.GROUP_ID)); if (tsOfficeId != null) { - joinCond = joinCond.and(grpAssgn.DB_OFFICE_ID.eq(tsOfficeId)); + joinCond = joinCond.and(grpAssgn.DB_OFFICE_ID.eq(tsOfficeId.toUpperCase())); } SelectSeekStep4>, String, String, String, String> query = dsl + List>, String, String, String, String> query = dslContext .select( catGrp.CAT_DB_OFFICE_ID, catGrp.TS_CATEGORY_ID, @@ -137,7 +190,7 @@ private List getTimeSeriesGroupsWhere(Condition whereCond, Stri catGrp.SHARED_TS_ALIAS_ID, catGrp.SHARED_REF_TS_ID, DSL.multiset( - dsl + dslContext .select( grpAssgn.TS_ID, grpAssgn.DB_OFFICE_ID, @@ -233,12 +286,10 @@ private TimeSeriesCategory buildTimeSeriesCategory(org.jooq.Record queryRecord) return new TimeSeriesCategory(catOfficeId, catId, catDesc); } - private Condition buildWhereCondition(String categoryId, String groupId) { AV_TS_CAT_GRP atcg = AV_TS_CAT_GRP.AV_TS_CAT_GRP; Condition whereCondition = DSL.noCondition(); - if (categoryId != null && !categoryId.isEmpty()) { whereCondition = whereCondition.and(atcg.TS_CATEGORY_ID.eq(categoryId)); } @@ -249,41 +300,180 @@ private Condition buildWhereCondition(String categoryId, String groupId) { return whereCondition; } + public void delete(String categoryId, String groupId, String office, boolean cascade) { + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + // If caller didn't ask for cascade behavior, we can always use the legacy routine. + if (!cascade) { + CWMS_TS_PACKAGE.call_DELETE_TS_GROUP(dslContext.configuration(), categoryId, groupId, office); + return; + } - public void delete(String categoryId, String groupId, String office) { - connection(dsl, c -> - CWMS_TS_PACKAGE.call_DELETE_TS_GROUP( - getDslContext(c,office).configuration(), categoryId, groupId, office - ) - ); + // Cascade requested: + // 1) Prefer DELETE_TS_GROUP_CASCADE when it exists and binds successfully. + // 2) If it does not exist / doesn't bind, fall back to the legacy method (unassign + delete). + DeleteTsGroupCascadeMode mode = deleteTsGroupCascadeMode; + + if (mode == DeleteTsGroupCascadeMode.USE_CASCADE_ROUTINE) { + call_DELETE_TS_GROUP_CASCADE(dslContext.configuration(), categoryId, groupId, formatBool(true), office); + return; + } + + if (mode == DeleteTsGroupCascadeMode.USE_UNASSIGN) { + deleteViaUnassign(dslContext, categoryId, groupId, office, true); + return; + } + + // UNKNOWN: just try it; harmless if multiple threads probe simultaneously. + try { + call_DELETE_TS_GROUP_CASCADE(dslContext.configuration(), categoryId, groupId, formatBool(true), office); + deleteTsGroupCascadeMode = DeleteTsGroupCascadeMode.USE_CASCADE_ROUTINE; + } catch (RuntimeException e) { + if (isMissingOrBindFailure(e)) { + // No reason to log the whole exception here. It was either missing or bind + // and we think we have an alternative. + logger.atFine().log("DELETE_TS_GROUP_CASCADE is not available. Falling back to iterative cascade delete."); + deleteTsGroupCascadeMode = DeleteTsGroupCascadeMode.USE_UNASSIGN; + deleteViaUnassign(dslContext, categoryId, groupId, office, true); + } else { + throw e; + } + } + }); } - public void create(TimeSeriesGroup group, boolean failIfExists) { + /** + * + * @param dslContext a jooq context that already has the approriate office set in the session. + * @param categoryId + * @param groupId + * @param office + * @param cascade + */ + private void deleteViaUnassign(DSLContext dslContext, String categoryId, String groupId, String office, boolean cascade) { + + dslContext.transaction((Configuration config) -> { + + if (cascade) { + unassignAll(config, categoryId, groupId, office); + } + CWMS_TS_PACKAGE.call_DELETE_TS_GROUP(config, categoryId, groupId, office); + }); + } + + + public void unassignAll(String categoryId, String groupId, String office) { + dsl.transaction((Configuration config) -> { + unassignAll(config, categoryId, groupId, office); + }); + } + + // This may not be that useful in practice. Typically groups either below to an office like SPK or to CWMS. + // Offices can assign ts to the CWMS group and they should be able to unassign their own ts from a CWMS group + // but SPK users shouldn't be able to unassign assignments that belong to other offices (SWT) and they + // shouldn't be able to remove CWMS assignments. SPK users also shouldn't be able to delete CWMS groups. + // + // This method is currently used when cascade delete isn't available in the pl/sql and the user wants + // to unassign all assignments for a group so that the group will be empty and can then be deleted. + // In practice, CWMS groups can't be deleted, even if they were empty. So this would only be helpful + // for office specific groups, in which case users only need to unassign for a single office (their own). + private void unassignAll(Configuration config, String categoryId, String groupId, String office) { + DSLContext context = config.dsl(); + + // Find all the offices with an assignment in the group. + List assignmentOffices = getAssignmentOffices(context, categoryId, groupId, office); + logger.atInfo().log("For o:%s c:%s g:%s found assignments in offices:%s", office, categoryId, groupId, assignmentOffices); + if (!assignmentOffices.isEmpty()) { + for (String assignmentOffice : assignmentOffices) { + unassignForOffice(config, categoryId, groupId, office, assignmentOffice); + } + } + } + + public void unassignForOffice( String categoryId, String groupId, String office, String assignmentOffice) { + connection(dsl, conn -> { + DSLContext dslContext = getDslContext(conn, office); + unassignForOffice(dslContext.configuration(), categoryId, groupId, office, assignmentOffice); + }); + } + + public static void unassignForOffice(Configuration config, String categoryId, String groupId, String office, String assignmentOffice) { + if (office != null && !"CWMS".equals(office)) { + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(config, assignmentOffice); + } + CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP(config, + categoryId, groupId, + null, "T", assignmentOffice); + } + + private List getAssignmentOffices(DSLContext context, String categoryId, String groupId, String office) { + List retval = new ArrayList<>(); + + // retrieve with a null tsOfficeId so that we get ALL ts assignments. + TimeSeriesGroup group = getTimeSeriesGroup(context,null, office, null, categoryId, groupId); + if (group != null) { + + Set assignmentOffices = new LinkedHashSet<>(); + if (group.getAssignedTimeSeries() != null) { + for (AssignedTimeSeries ats : group.getAssignedTimeSeries()) { + assignmentOffices.add(ats.getOfficeId()); + } + } + + boolean hadCwms = assignmentOffices.remove("CWMS"); + retval.addAll(assignmentOffices); + if (hadCwms) { + assignmentOffices.add("CWMS"); // want it last + } + } + return retval; + } + + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + public static void call_DELETE_TS_GROUP_CASCADE(Configuration configuration, String tsCategoryId, String tsGroupId, String cascade, String dbOfficeId) { + DELETE_TS_GROUP_CASCADE p = new DELETE_TS_GROUP_CASCADE(); // This is our own routine, not codegen. + p.setP_TS_CATEGORY_ID(tsCategoryId); + p.setP_TS_GROUP_ID(tsGroupId); + p.setP_CASCADE(cascade); + p.setP_DB_OFFICE_ID(dbOfficeId); + p.execute(configuration); + } + + + public void create(TimeSeriesGroup group, boolean failIfExists, boolean ignoreNulls) { connection(dsl, c -> { - Configuration configuration = getDslContext(c,group.getOfficeId()).configuration(); + Configuration configuration = getDslContext(c, group.getOfficeId()).configuration(); String categoryId = group.getTimeSeriesCategory().getId(); CWMS_TS_PACKAGE.call_STORE_TS_GROUP(configuration, categoryId, group.getId(), group.getDescription(), formatBool(failIfExists), - "T", group.getSharedAliasId(), + formatBool(ignoreNulls), group.getSharedAliasId(), group.getSharedRefTsId(), group.getOfficeId()); - assignTs(configuration,group, group.getOfficeId()); + assignTs(configuration, group, group.getOfficeId(), ignoreNulls); }); } - private void assignTs(Configuration configuration,TimeSeriesGroup group, String office) { + private void assignTs(Configuration configuration, TimeSeriesGroup group, String office, boolean ignoreNulls) { List assignedTimeSeries = group.getAssignedTimeSeries(); - if (assignedTimeSeries != null) { - List collect = assignedTimeSeries.stream() - .map(TimeSeriesGroupDao::convertToTsAliasType) - .collect(toList()); - TS_ALIAS_TAB_T assignedLocs = new TS_ALIAS_TAB_T(collect); - CWMS_TS_PACKAGE.call_ASSIGN_TS_GROUPS(configuration, group.getTimeSeriesCategory().getId(), - group.getId(), assignedLocs, office); + + if (!ignoreNulls && (assignedTimeSeries == null || assignedTimeSeries.isEmpty())) { + CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP(configuration, + group.getTimeSeriesCategory().getId(), group.getId(), + null, "T", group.getOfficeId()); + + } else { + if (assignedTimeSeries != null) { + List collect = assignedTimeSeries.stream() + .map(TimeSeriesGroupDao::convertToTsAliasType) + .collect(toList()); + TS_ALIAS_TAB_T assignedLocs = new TS_ALIAS_TAB_T(collect); + CWMS_TS_PACKAGE.call_ASSIGN_TS_GROUPS(configuration, group.getTimeSeriesCategory().getId(), + group.getId(), assignedLocs, office); + } } } public void assignTs(TimeSeriesGroup group, String office) { - connection(dsl, c -> assignTs(getDslContext(c, office).configuration(),group, office)); + connection(dsl, c -> assignTs(getDslContext(c, office).configuration(),group, office, true)); } private static TS_ALIAS_T convertToTsAliasType(AssignedTimeSeries assignedTimeSeries) { @@ -301,12 +491,24 @@ public void renameTimeSeriesGroup(String oldGroupId, TimeSeriesGroup group) { ); } + public void unassignAllTs(TimeSeriesGroup group, String officeId) { + connection(dsl, c -> - CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP( - getDslContext(c,officeId).configuration(), - group.getTimeSeriesCategory().getId(), group.getId(), - null, "T", officeId) + // For Default/Default if officeId is 'CWMS' this seems to not unassign + // the assigned timeseries where the office id of the timeseries is SPK. + // Is this a bug? + { + DSLContext dslContext = getDslContext(c, officeId); + + // UNASSIGN_TS_GROUP apparently only unassigns the assignments that are in the + // P_DB_OFFICE_ID ( last parameter) + CWMS_TS_PACKAGE.call_UNASSIGN_TS_GROUP( + dslContext.configuration(), + group.getTimeSeriesCategory().getId(), group.getId(), + null, "T", group.getOfficeId()); + } ); } + } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java index 4012fda3c6..332f5c08ff 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/TimeSeriesIdentifierDescriptorDao.java @@ -94,7 +94,7 @@ public TimeSeriesIdentifierDescriptors getTimeSeriesIdentifiers(String cursor, i Condition whereCondition = AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.ALIASED_ITEM.isNull(); if (office != null && !office.isEmpty()) { - whereCondition = whereCondition.and(AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.DB_OFFICE_ID.equalIgnoreCase(office)); + whereCondition = whereCondition.and(AV_CWMS_TS_ID2.AV_CWMS_TS_ID2.DB_OFFICE_ID.eq(office.toUpperCase())); } if (idRegex != null && !idRegex.isEmpty()) { whereCondition = whereCondition.and( @@ -229,7 +229,7 @@ public Optional getTimeSeriesIdentifier(String o result = dsl.select(view.CWMS_TS_ID, view.DB_OFFICE_ID, view.INTERVAL, view.TIME_ZONE_ID, view.TS_ACTIVE_FLAG) .from(view) - .where(view.CWMS_TS_ID.eq(timeseriesId).and(view.DB_OFFICE_ID.eq(office))).fetchOne(); + .where(view.CWMS_TS_ID.eq(timeseriesId).and(view.DB_OFFICE_ID.eq(office.toUpperCase()))).fetchOne(); Optional retval = Optional.empty(); if (result != null) { retval = Optional.of(toDto(result)); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/UserDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/UserDao.java index fce14d29d2..794aca4614 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/UserDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/UserDao.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Optional; +import cwms.cda.data.dto.auth.users.UsersPageCursor; import org.jooq.CommonTableExpression; import org.jooq.Condition; import org.jooq.DSLContext; @@ -133,7 +134,7 @@ public List getRoles() { }); } - public Users getAll(String cursor, int pageSize, String office, boolean includeRoles) { + public Users getAll(String cursor, int pageSize, String office, boolean includeRoles, String usernameRegex) { final AV_SEC_USERS vUserGroups = AV_SEC_USERS.AV_SEC_USERS.as("ug"); final Table vUsers = AT_SEC_CWMS_USERS.as("ut"); final Field userId = field(name(vUsers.getName(),"USERID"), String.class); @@ -148,6 +149,7 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR String cursorUserId = null; int pageSizeTmp = pageSize; String limitOffice = null; + String pageUsernameRegex = usernameRegex; Condition whereClause = office == null ? DSL.noCondition() // If we are including only those users with permissions to a specific office @@ -158,6 +160,11 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR .and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1) ; + // Apply username regex filter (case-insensitive) if provided + if (usernameRegex != null && !usernameRegex.isEmpty()) { + whereClause = whereClause.and(JooqDao.caseInsensitiveLikeRegexNullTrue(userId, usernameRegex)); + } + if (cursor == null || cursor.isEmpty()) { SelectConditionStep> count = dsl.select(count(asterisk())) .from(vUsers) @@ -168,27 +175,27 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR total = rec.value1(); } } else { - final String[] parts = CwmsDTOPaginated.decodeCursor(cursor, "||"); + Optional pageCursorOpt = CwmsDTOPaginated.decodeCursor(cursor, UsersPageCursor.class); - logger.atFine().log("decoded cursor: " + String.join("||", parts)); - for (String p : parts) { - logger.atFinest().log(p); - } - - if (parts.length > 1) { - cursorUserId = parts[0]; - total = Integer.parseInt(parts[2]); - pageSizeTmp = Integer.parseInt(parts[1]); - limitOffice = parts[3].equals("null") ? null : parts[3]; + if(pageCursorOpt.isPresent()) { + UsersPageCursor pageCursor = pageCursorOpt.get(); + cursorUserId = pageCursor.getCursorUserId(); + total = pageCursor.getTotal(); + pageSizeTmp = pageCursor.getPageSize(); + limitOffice = pageCursor.getLimitOffice(); + pageUsernameRegex = pageCursor.getUsernameRegex(); // Rebuild the where clause to match the initial conditions whereClause = limitOffice == null ? DSL.noCondition() - // If we are including only those users with permissions to a specific office - // we limit to those users that also have an entry in the at_sec_cwms_users_group table. - : dsl.select(count(asterisk())) - .from(vUserGroups) - .where(upper(vUserGroups.DB_OFFICE_ID).eq(upper(limitOffice))) - .and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1); + // If we are including only those users with permissions to a specific office + // we limit to those users that also have an entry in the at_sec_cwms_users_group table. + : dsl.select(count(asterisk())) + .from(vUserGroups) + .where(upper(vUserGroups.DB_OFFICE_ID).eq(upper(limitOffice))) + .and(vUserGroups.IS_MEMBER.eq("T")).asField().gt(1); + if (pageUsernameRegex != null && !pageUsernameRegex.isEmpty()) { + whereClause = whereClause.and(JooqDao.caseInsensitiveLikeRegexNullTrue(userId, pageUsernameRegex)); + } } } @@ -230,7 +237,7 @@ public Users getAll(String cursor, int pageSize, String office, boolean includeR logger.atFine().log("%s", lazy(() -> query.getSQL(ParamType.INLINED))); - final Users.Builder builder = new Users.Builder(cursor, pageSizeTmp, total, limitOffice); + final Users.Builder builder = new Users.Builder(cursor, pageSizeTmp, total, limitOffice, pageUsernameRegex); final Map tmpUsers = new LinkedHashMap<>(); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java index 39aaef9b41..c23e573ede 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatum.java @@ -15,8 +15,8 @@ public enum VerticalDatum { public static VerticalDatum getVerticalDatum(String input) { VerticalDatum retval = null; - if (input != null) { - input = input.replace("-", ""); + if (input != null && !input.isBlank()) { + input = input.trim().replace("-", ""); retval = VerticalDatum.valueOf(input.toUpperCase()); } return retval; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java new file mode 100644 index 0000000000..1a072dbce1 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/VerticalDatumDao.java @@ -0,0 +1,114 @@ +/* + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.data.dao; + +import cwms.cda.api.errors.AlreadyExists; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.formatters.xml.XMLv1; +import org.jooq.DSLContext; +import org.jooq.Record1; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; +import usace.cwms.db.jooq.codegen.tables.AV_VERT_DATUM_OFFSET; + +/** + * DAO responsible for CRUD operations on Vertical Datum Info for a Location. + */ +public final class VerticalDatumDao extends JooqDao { + + public VerticalDatumDao(DSLContext dsl) { + super(dsl); + } + + public VerticalDatumInfo retrieveVerticalDatumInfo(String officeId, String locationId, String unit) { + return connectionResult(dsl, conn -> { + DSLContext ctx = getDslContext(conn, officeId); + //using jooq to check if exists, because the package get was adding to view if it didn't exist + verifyVerticalDatumInfoExists(ctx, officeId, locationId); + String xml = CWMS_LOC_PACKAGE.call_GET_VERTICAL_DATUM_INFO_F__2(ctx.configuration(), locationId, unit, officeId); + return TimeSeriesDaoImpl.parseVerticalDatumInfo(xml); + }); + } + + public void createVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) { + connection(dsl, conn -> { + DSLContext ctx = getDslContext(conn, officeId); + verifyVerticalDatumInfoDoesNotExist(ctx, officeId, locationId); + store(ctx, officeId, locationId, vdi); + }); + } + + public void updateVerticalDatumInfo(String officeId, String locationId, VerticalDatumInfo vdi) { + connection(dsl, conn -> { + DSLContext ctx = getDslContext(conn, officeId); + verifyVerticalDatumInfoExists(ctx, officeId, locationId); + store(ctx, officeId, locationId, vdi); + }); + } + + private void store(DSLContext ctx, String officeId, String locationId, VerticalDatumInfo vdi) { + //we pass in the location and office, and including them in the xml causes store issues in the db so, removing since they aren't necessary + VerticalDatumInfo vdiWithoutLocAndOffice = new VerticalDatumInfo.Builder().from(vdi) + .withOffice(null) + .withLocation(null) + .build(); + String xml = new XMLv1().format(vdiWithoutLocAndOffice); + CWMS_LOC_PACKAGE.call_SET_VERTICAL_DATUM_INFO__3(ctx.configuration(), locationId, xml, formatBool(false), officeId); + } + + + public void deleteVerticalDatumInfo(String officeId, String locationId) { + connection(dsl, conn -> { + DSLContext ctx = getDslContext(conn, officeId); + verifyVerticalDatumInfoExists(ctx, officeId, locationId); + VerticalDatumInfo emptyVdi = new VerticalDatumInfo.Builder() + .withLocation(locationId) + .withOffice(officeId) + .withUnit("m") + .build(); + String emptyXml = new XMLv1().format(emptyVdi); + CWMS_LOC_PACKAGE.call_SET_VERTICAL_DATUM_INFO(ctx.configuration(), emptyXml, formatBool(false)); + CWMS_LOC_PACKAGE.call_DELETE_LOCAL_VERT_DATUM_NAME__2(ctx.configuration(), locationId, officeId); + }); + } + + private void verifyVerticalDatumInfoExists(DSLContext ctx, String officeId, String locationId) { + Record1 result = ctx.select(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET).from(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET) + .where(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.LOCATION_ID.eq(locationId)) + .and(AV_VERT_DATUM_OFFSET.AV_VERT_DATUM_OFFSET.OFFICE_ID.eq(officeId)) + .fetchOne(); + if(result == null) { + throw new NotFoundException("No vertical datum info found for location " + locationId + " in office " + officeId); + } + } + + private void verifyVerticalDatumInfoDoesNotExist(DSLContext ctx, String officeId, String locationId) { + try { + verifyVerticalDatumInfoExists(ctx, officeId, locationId); + } catch (NotFoundException e) { + return; + } + throw new AlreadyExists("Vertical datum info already exists for location " + locationId + " in office " + officeId, null); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/project/ProjectDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/project/ProjectDao.java index 8b670c34ed..4d3783673e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/project/ProjectDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/project/ProjectDao.java @@ -142,6 +142,12 @@ public Projects retrieveProjects(String cursor, @Nullable String office, @Nullab final String cursorOffice; final String cursorProjectId; int total; + + if(office != null){ + office = office.toUpperCase(); + } + String finalOffice = office; + if (cursor == null || cursor.isEmpty()) { cursorOffice = null; cursorProjectId = null; @@ -149,8 +155,8 @@ public Projects retrieveProjects(String cursor, @Nullable String office, @Nullab Condition whereClause = JooqDao.caseInsensitiveLikeRegexNullTrue(AV_PROJECT.AV_PROJECT.PROJECT_ID, projectIdMask); - if (office != null) { - whereClause = whereClause.and(AV_PROJECT.AV_PROJECT.OFFICE_ID.eq(office)); + if (finalOffice != null) { + whereClause = whereClause.and(AV_PROJECT.AV_PROJECT.OFFICE_ID.eq(finalOffice)); } SelectConditionStep> count = @@ -168,13 +174,13 @@ public Projects retrieveProjects(String cursor, @Nullable String office, @Nullab // There are lots of ways the variables can be null or not so we need to build the query // based on the parameters. - String query = buildTableQuery(office, projectIdMask, cursorOffice != null || cursorProjectId != null); + String query = buildTableQuery(finalOffice, projectIdMask, cursorOffice != null || cursorProjectId != null); int finalPageSize = pageSize; List projs = connectionResult(dsl, c -> { List projects; try (PreparedStatement ps = c.prepareStatement(query)) { - fillTableQueryParameters(ps, cursorOffice, cursorProjectId, office, projectIdMask, finalPageSize); + fillTableQueryParameters(ps, cursorOffice, cursorProjectId, finalOffice, projectIdMask, finalPageSize); try (ResultSet resultSet = ps.executeQuery()) { projects = new ArrayList<>(); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/rss/MessageDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/rss/MessageDao.java index f6afa28972..b506236f6f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/rss/MessageDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/rss/MessageDao.java @@ -33,6 +33,7 @@ import static org.jooq.impl.DSL.select; import static org.jooq.impl.DSL.table; +import cwms.cda.api.enums.MessageQueue; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.CwmsDTOPaginated; @@ -44,7 +45,6 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.function.UnaryOperator; import org.jooq.DSLContext; @@ -62,7 +62,7 @@ public MessageDao(DSLContext dsl) { public RssFeed retrieveFeed(String cursor, int pageSize, String office, String name, Instant since, UnaryOperator urlBuilder) { - AqTable aqTable = getAqTable(name); + MessageQueue aqTable = getAqTable(name); String[] cursorSplit = CwmsDTOPaginated.decodeCursor(cursor); int offset = 0; if(cursorSplit.length == 2) { @@ -83,30 +83,18 @@ public RssFeed retrieveFeed(String cursor, int pageSize, String office, String n String nextCursor = CwmsDTOPaginated.encodeCursor(items.size() + offset, pageSize); nextLink = new AtomLink("next", urlBuilder.apply(nextCursor)); } - String description; - switch(aqTable) { - case TS_STORED: - description = " CWMS messages about time series operations, such as data stored and deleted"; - break; - case STATUS: - description = " CWMS general system and application status messages"; - break; - case REALTIME_OPS: - description = " CWMS application operational messages"; - break; - default: - description = null; - } + String description = aqTable.description(); RssChannel channel = new RssChannel(name, nextLink, description, items); return new RssFeed(channel); } - private static AqTable getAqTable(String name) { - try { - return AqTable.valueOf(name.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new NotFoundException(e); + @SuppressWarnings("unused") // MessageQueue.valueOf can return null. environment is being over zealous. + private static MessageQueue getAqTable(String name) { + MessageQueue ret = MessageQueue.queueFor(name.toUpperCase()); + if (ret == null) { + throw new NotFoundException("No queue named '" + name + "'"); } + return ret; } private static RssItem rssItem(Record record, String p) { @@ -116,7 +104,7 @@ private static RssItem rssItem(Record record, String p) { return new RssItem(p, enqTimestamp, msgId); } - private Result retrieveMessages(int offset, int pageSize, Instant since, String office, AqTable name) { + private Result retrieveMessages(int offset, int pageSize, Instant since, String office, MessageQueue name) { Timestamp sinceTimestamp = since == null ? null : Timestamp.from(since); Table t = table(name("CWMS_20", "AQ$" + office + "_" + name.name() + "_TABLE")).as("t"); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesgroup/DELETE_TS_GROUP_CASCADE.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesgroup/DELETE_TS_GROUP_CASCADE.java new file mode 100644 index 0000000000..961d042a5a --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesgroup/DELETE_TS_GROUP_CASCADE.java @@ -0,0 +1,48 @@ +package cwms.cda.data.dao.timeseriesgroup; + +import org.jooq.Parameter; +import org.jooq.impl.AbstractRoutine; +import org.jooq.impl.DSL; +import org.jooq.impl.Internal; +import org.jooq.impl.SQLDataType; +import usace.cwms.db.jooq.codegen.CWMS_20; +import usace.cwms.db.jooq.codegen.packages.CWMS_TS_PACKAGE; + +public class DELETE_TS_GROUP_CASCADE extends AbstractRoutine { + private static final long serialVersionUID = 1L; + public static final Parameter P_TS_CATEGORY_ID; + public static final Parameter P_TS_GROUP_ID; + public static final Parameter P_CASCADE; + public static final Parameter P_DB_OFFICE_ID; + + public DELETE_TS_GROUP_CASCADE() { + super("DELETE_TS_GROUP_CASCADE", CWMS_20.CWMS_20, CWMS_TS_PACKAGE.CWMS_TS); + this.addInParameter(P_TS_CATEGORY_ID); + this.addInParameter(P_TS_GROUP_ID); + this.addInParameter(P_CASCADE); + this.addInParameter(P_DB_OFFICE_ID); + } + + public void setP_TS_CATEGORY_ID(String value) { + this.setValue(P_TS_CATEGORY_ID, value); + } + + public void setP_TS_GROUP_ID(String value) { + this.setValue(P_TS_GROUP_ID, value); + } + + public void setP_CASCADE(String value) { + this.setValue(P_CASCADE, value); + } + + public void setP_DB_OFFICE_ID(String value) { + this.setValue(P_DB_OFFICE_ID, value); + } + + static { + P_TS_CATEGORY_ID = Internal.createParameter("P_TS_CATEGORY_ID", SQLDataType.VARCHAR, false, false); + P_TS_GROUP_ID = Internal.createParameter("P_TS_GROUP_ID", SQLDataType.VARCHAR, false, false); + P_CASCADE = Internal.createParameter("P_CASCADE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_DB_OFFICE_ID = Internal.createParameter("P_DB_OFFICE_ID", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + } +} \ No newline at end of file diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesprofile/TimeSeriesProfileInstanceDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesprofile/TimeSeriesProfileInstanceDao.java index da9789a792..76e04d8388 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesprofile/TimeSeriesProfileInstanceDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/timeseriesprofile/TimeSeriesProfileInstanceDao.java @@ -10,6 +10,7 @@ import static org.jooq.impl.DSL.using; import static org.jooq.impl.DSL.val; +import com.google.common.flogger.FluentLogger; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.CwmsDTOPaginated; @@ -28,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import com.google.common.flogger.FluentLogger; import org.jetbrains.annotations.NotNull; import org.jooq.Condition; import org.jooq.DSLContext; @@ -240,8 +240,7 @@ public TimeSeriesProfileInstance retrieveTimeSeriesProfileInstance(CwmsId locati final String[] parts = CwmsDTOPaginated.decodeCursor(page); logger.atFine().log("Decoded cursor"); - logger.atFinest().log("%s", lazy(()-> - { + logger.atFinest().log("%s", lazy(() -> { StringBuilder sb = new StringBuilder(); for (String part : parts) { sb.append(part).append("\n"); @@ -325,20 +324,20 @@ public TimeSeriesProfileInstance retrieveTimeSeriesProfileInstance(CwmsId locati // Add the time windows conditions depending on the inclusive flags if (startInclusive && endInclusive) { whereCondition = whereCondition - .and(VIEW_TSV2.FIRST_DATE_TIME.ge(Timestamp.from(startTime))) - .and(VIEW_TSV2.LAST_DATE_TIME.le(Timestamp.from(endTime))); + .and(VIEW_TSV2.DATE_TIME.ge(Timestamp.from(startTime))) + .and(VIEW_TSV2.DATE_TIME.le(Timestamp.from(endTime))); } else if (!startInclusive && endInclusive) { whereCondition = whereCondition - .and(VIEW_TSV2.FIRST_DATE_TIME.greaterThan(Timestamp.from(startTime))) - .and(VIEW_TSV2.LAST_DATE_TIME.le(Timestamp.from(endTime))); + .and(VIEW_TSV2.DATE_TIME.greaterThan(Timestamp.from(startTime))) + .and(VIEW_TSV2.DATE_TIME.le(Timestamp.from(endTime))); } else if (startInclusive) { whereCondition = whereCondition - .and(VIEW_TSV2.FIRST_DATE_TIME.ge(Timestamp.from(startTime))) - .and(VIEW_TSV2.LAST_DATE_TIME.lessThan(Timestamp.from(endTime))); + .and(VIEW_TSV2.DATE_TIME.ge(Timestamp.from(startTime))) + .and(VIEW_TSV2.DATE_TIME.lessThan(Timestamp.from(endTime))); } else { whereCondition = whereCondition - .and(VIEW_TSV2.FIRST_DATE_TIME.greaterThan(Timestamp.from(startTime))) - .and(VIEW_TSV2.LAST_DATE_TIME.lessThan(Timestamp.from(endTime))); + .and(VIEW_TSV2.DATE_TIME.greaterThan(Timestamp.from(startTime))) + .and(VIEW_TSV2.DATE_TIME.lessThan(Timestamp.from(endTime))); } Condition finalWhereCondition = whereCondition; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDao.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDao.java index c8add1f6e3..659afb4c86 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDao.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDao.java @@ -32,12 +32,16 @@ import cwms.cda.data.dto.watersupply.WaterUser; import hec.lang.Const; import java.math.BigInteger; +import java.sql.Connection; +import java.sql.SQLException; import java.sql.Timestamp; import java.time.Instant; import java.util.ArrayList; import java.util.List; import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; +import org.jspecify.annotations.NonNull; import usace.cwms.db.jooq.codegen.packages.CWMS_WATER_SUPPLY_PACKAGE; import usace.cwms.db.jooq.codegen.udt.records.LOC_REF_TIME_WINDOW_TAB_T; import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; @@ -57,18 +61,58 @@ public void storeAccounting(WaterSupplyAccounting accounting) { connection(dsl, c -> { setOffice(c, accounting.getWaterUser().getProjectId().getOfficeId()); - - WAT_USR_CONTRACT_ACCT_TAB_T accountingTab = WaterSupplyUtils.toWaterUserContractAcctTs(accounting); - WATER_USER_CONTRACT_REF_T contractRefT = WaterSupplyUtils - .toContractRef(accounting.getWaterUser(), accounting.getContractName()); - LOC_REF_TIME_WINDOW_TAB_T pumpTimeWindowTab = WaterSupplyUtils.toTimeWindowTabT(accounting); - String timeZoneId = "UTC"; - String overrideProt = formatBool(overrideProtection); - CWMS_WATER_SUPPLY_PACKAGE.call_STORE_ACCOUNTING_SET(DSL.using(c).configuration(), accountingTab, - contractRefT, pumpTimeWindowTab, timeZoneId, volumeUnitId, storeRule, overrideProt); + try { + storeViaCodegen(c, accounting, overrideProtection, volumeUnitId, storeRule); + } catch (DataAccessException e) { + if (isBindException(e)) { + try { + storeViaManual(c, accounting, overrideProtection, volumeUnitId, storeRule); + return; + } catch (Exception e2) { + RuntimeException re = new RuntimeException(e); + re.addSuppressed(e2); + throw re; + } + } + throw e; + } }); } + private static boolean isBindException(DataAccessException e) { + boolean isBind = false; + Throwable cause = e.getCause(); + if( cause instanceof SQLException){ + SQLException se = (SQLException)cause; + String sqlMessage = se.getMessage(); + Throwable sqlCause = se.getCause(); + isBind = sqlCause instanceof IllegalArgumentException && sqlMessage.contains("Error while writing value"); + } + return isBind; + } + + private static void storeViaCodegen(Connection c, WaterSupplyAccounting accounting, boolean overrideProtection, String volumeUnitId, String storeRule) { + WAT_USR_CONTRACT_ACCT_TAB_T accountingTab = WaterSupplyUtils.toWaterUserContractAcctTs(accounting); + WATER_USER_CONTRACT_REF_T contractRefT = WaterSupplyUtils + .toContractRef(accounting.getWaterUser(), accounting.getContractName()); + LOC_REF_TIME_WINDOW_TAB_T pumpTimeWindowTab = WaterSupplyUtils.toTimeWindowTabT(accounting); + String timeZoneId = "UTC"; + String overrideProt = formatBool(overrideProtection); + CWMS_WATER_SUPPLY_PACKAGE.call_STORE_ACCOUNTING_SET(DSL.using(c).configuration(), accountingTab, + contractRefT, pumpTimeWindowTab, timeZoneId, volumeUnitId, storeRule, overrideProt); + } + + private static void storeViaManual(Connection c, WaterSupplyAccounting accounting, boolean overrideProtection, String volumeUnitId, String storeRule) { + cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T accountingTab = WaterSupplyUtils.toManualWaterUserContractAcctTs(accounting); + WATER_USER_CONTRACT_REF_T contractRefT = WaterSupplyUtils + .toContractRef(accounting.getWaterUser(), accounting.getContractName()); + LOC_REF_TIME_WINDOW_TAB_T pumpTimeWindowTab = WaterSupplyUtils.toTimeWindowTabT(accounting); + String timeZoneId = "UTC"; + String overrideProt = formatBool(overrideProtection); + cwms.cda.data.dao.watersupply.handgen.CWMS_WATER_SUPPLY_PACKAGE.call_STORE_ACCOUNTING_SET(DSL.using(c).configuration(), accountingTab, + contractRefT, pumpTimeWindowTab, timeZoneId, volumeUnitId, storeRule, overrideProt); + } + public List retrieveAccounting(String contractName, WaterUser waterUser, CwmsId projectLocation, String units, Instant startTime, Instant endTime, boolean startInclusive, boolean endInclusive, boolean ascendingFlag, int rowLimit) { @@ -85,15 +129,51 @@ public List retrieveAccounting(String contractName, Water return connectionResult(dsl, c -> { setOffice(c, projectLocation.getOfficeId()); - WAT_USR_CONTRACT_ACCT_TAB_T watUsrContractAcctObjTs - = CWMS_WATER_SUPPLY_PACKAGE.call_RETRIEVE_ACCOUNTING_SET(DSL.using(c).configuration(), - contractRefT, units, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag, - endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType); - if (!watUsrContractAcctObjTs.isEmpty()) { - return WaterSupplyUtils.toWaterSupplyAccountingList(c, watUsrContractAcctObjTs); - } else { - return new ArrayList<>(); + try { + return retrieveFromCodegen(units, c, contractRefT, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag, endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType); + } catch (DataAccessException e){ + if(isInvalidColumn(e)){ + return retrieveViaManual(units, c, contractRefT, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag, endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType); + } + throw e; } }); } + + private @NonNull List retrieveViaManual(String units, Connection c, WATER_USER_CONTRACT_REF_T contractRefT, Timestamp startTimestamp, Timestamp endTimestamp, String timeZoneId, String startInclusiveFlag, String endInclusiveFlag, String ascendingFlagStr, BigInteger rowLimitBigInt, String transferType) { + cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T watUsrContractAcctObjTs + = cwms.cda.data.dao.watersupply.handgen.CWMS_WATER_SUPPLY_PACKAGE.call_RETRIEVE_ACCOUNTING_SET(DSL.using(c).configuration(), + contractRefT, units, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag, + endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType); + if (!watUsrContractAcctObjTs.isEmpty()) { + return WaterSupplyUtils.toWaterSupplyAccountingList(c, watUsrContractAcctObjTs); + } else { + return new ArrayList<>(); + } + } + + private boolean isInvalidColumn(DataAccessException e) { + boolean isInvalid = false; + + Throwable cause = e.getCause(); + if( cause instanceof SQLException){ + SQLException se = (SQLException)cause; + String sqlMessage = se.getMessage(); + isInvalid = sqlMessage.contains("Invalid column"); + } + + return isInvalid; + } + + private static @NonNull List retrieveFromCodegen(String units, Connection c, WATER_USER_CONTRACT_REF_T contractRefT, Timestamp startTimestamp, Timestamp endTimestamp, String timeZoneId, String startInclusiveFlag, String endInclusiveFlag, String ascendingFlagStr, BigInteger rowLimitBigInt, String transferType) { + WAT_USR_CONTRACT_ACCT_TAB_T watUsrContractAcctObjTs + = CWMS_WATER_SUPPLY_PACKAGE.call_RETRIEVE_ACCOUNTING_SET(DSL.using(c).configuration(), + contractRefT, units, startTimestamp, endTimestamp, timeZoneId, startInclusiveFlag, + endInclusiveFlag, ascendingFlagStr, rowLimitBigInt, transferType); + if (!watUsrContractAcctObjTs.isEmpty()) { + return WaterSupplyUtils.toWaterSupplyAccountingList(c, watUsrContractAcctObjTs, units); + } else { + return new ArrayList<>(); + } + } } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyUtils.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyUtils.java index b3478e08e8..5b578d165e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyUtils.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/WaterSupplyUtils.java @@ -26,6 +26,7 @@ package cwms.cda.data.dao.watersupply; +import com.google.common.flogger.FluentLogger; import cwms.cda.data.dao.Dao; import cwms.cda.data.dao.location.kind.LocationUtil; import cwms.cda.data.dto.CwmsId; @@ -46,9 +47,13 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import com.google.common.flogger.FluentLogger; +import mil.army.usace.hec.metadata.DataSetIllegalArgumentException; +import mil.army.usace.hec.metadata.Parameter; +import mil.army.usace.hec.metadata.UnitUtil; +import mil.army.usace.hec.metadata.UnitsConversionException; import org.jetbrains.annotations.NotNull; import org.jooq.impl.DSL; +import org.jspecify.annotations.NonNull; import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; import usace.cwms.db.jooq.codegen.udt.records.LOC_REF_TIME_WINDOW_OBJ_T; import usace.cwms.db.jooq.codegen.udt.records.LOC_REF_TIME_WINDOW_TAB_T; @@ -62,20 +67,63 @@ import usace.cwms.db.jooq.codegen.udt.records.WAT_USR_CONTRACT_ACCT_TAB_T; -final class WaterSupplyUtils { - private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); +public final class WaterSupplyUtils { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private WaterSupplyUtils() { throw new IllegalStateException("Utility class"); } + /** + * Converts all PumpTransfer flow values to SI units and returns a new WaterSupplyAccounting instance. + * The SI flow units are determined using metadata for the FLOW parameter. If any conversion fails, + * an IllegalArgumentException is thrown wrapping the UnitsConversionException. If the SI units + * cannot be determined due to metadata issues, a DataSetIllegalArgumentException may be thrown. + * + * @param accounting the input WaterSupplyAccounting, possibly containing non-SI flow units + * @return a new WaterSupplyAccounting with all flows in SI units + * @throws DataSetIllegalArgumentException if SI flow units cannot be determined + * @throws IllegalArgumentException if a units conversion fails + */ + public static WaterSupplyAccounting convertAccountingFlowsToSi(WaterSupplyAccounting accounting) + throws DataSetIllegalArgumentException, IllegalArgumentException { + String siFlowUnits = Parameter.getParameter(Parameter.PARAMID_FLOW) + .getUnitsStringForSystem(UnitUtil.SI_ID); + + Map> converted = new java.util.TreeMap<>(); + for (Map.Entry> entry : accounting.getPumpAccounting().entrySet()) { + List transformed = new java.util.ArrayList<>(); + for (PumpTransfer pt : entry.getValue()) { + String fromUnits = pt.getFlowUnit(); + if (fromUnits != null && !fromUnits.equalsIgnoreCase(siFlowUnits)) { + try { + double siValue = UnitUtil.convertUnits(pt.getFlow(), fromUnits, siFlowUnits); + transformed.add(new PumpTransfer(pt.getPumpType(), pt.getTransferTypeDisplay(), siValue, + siFlowUnits, pt.getComment())); + } catch (UnitsConversionException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } else { + transformed.add(pt); + } + } + converted.put(entry.getKey(), transformed); + } + return new WaterSupplyAccounting.Builder() + .withWaterUser(accounting.getWaterUser()) + .withContractName(accounting.getContractName()) + .withPumpLocations(accounting.getPumpLocations()) + .withPumpAccounting(converted) + .build(); + } + static WaterUserContract toWaterContract(WATER_USER_CONTRACT_OBJ_T contract) { Instant effectiveDate = null; - if(contract.getWS_CONTRACT_EFFECTIVE_DATE() != null) { + if (contract.getWS_CONTRACT_EFFECTIVE_DATE() != null) { effectiveDate = contract.getWS_CONTRACT_EFFECTIVE_DATE().toInstant(); } Instant expirationDate = null; - if(contract.getWS_CONTRACT_EXPIRATION_DATE() != null) { + if (contract.getWS_CONTRACT_EXPIRATION_DATE() != null) { expirationDate = contract.getWS_CONTRACT_EXPIRATION_DATE().toInstant(); } return new WaterUserContract.Builder().withContractedStorage(contract.getCONTRACTED_STORAGE()) @@ -185,40 +233,46 @@ static WAT_USR_CONTRACT_ACCT_TAB_T toWaterUserContractAcctTs(WaterSupplyAccounti for (Map.Entry> entry : accounting.getPumpAccounting().entrySet()) { for (PumpTransfer transfer : entry.getValue()) { - WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT = new WAT_USR_CONTRACT_ACCT_OBJ_T(); - WATER_USER_CONTRACT_REF_T contractRef = toContractRef(accounting.getWaterUser(), - accounting.getContractName()); - watUsrContractAcctObjT.setWATER_USER_CONTRACT_REF(contractRef); - watUsrContractAcctObjT.setACCOUNTING_REMARKS(transfer.getComment()); - watUsrContractAcctObjT.setPUMP_FLOW(transfer.getFlow()); - LOOKUP_TYPE_OBJ_T transferType = toLookupTypeO(new LookupType.Builder() - .withDisplayValue(transfer.getTransferTypeDisplay()) - .withActive(true) - .withOfficeId(accounting.getWaterUser().getProjectId().getOfficeId()) - .build()); - watUsrContractAcctObjT.setPHYSICAL_TRANSFER_TYPE(transferType); - switch (transfer.getPumpType()) { - case IN: - watUsrContractAcctObjT.setPUMP_LOCATION_REF(pumpIn); - break; - case OUT: - watUsrContractAcctObjT.setPUMP_LOCATION_REF(pumpOut); - break; - case BELOW: - watUsrContractAcctObjT.setPUMP_LOCATION_REF(pumpBelow); - break; - default: - LOGGER.atWarning().log("Invalid pump type"); - throw new IllegalArgumentException( - String.format("Invalid pump type for mapping to DB object: %s", transfer.getPumpType())); - } - watUsrContractAcctObjT.setTRANSFER_START_DATETIME(Timestamp.from(entry.getKey())); + WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT = getWatUsrContractAcctObjT(accounting, entry, transfer, pumpIn, pumpOut, pumpBelow); watUsrContractAcctObjTList.add(watUsrContractAcctObjT); } } return new WAT_USR_CONTRACT_ACCT_TAB_T(watUsrContractAcctObjTList); } + private static @NonNull WAT_USR_CONTRACT_ACCT_OBJ_T getWatUsrContractAcctObjT(WaterSupplyAccounting accounting, Map.Entry> entry, PumpTransfer transfer, LOCATION_REF_T pumpIn, LOCATION_REF_T pumpOut, LOCATION_REF_T pumpBelow) { + WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT = new WAT_USR_CONTRACT_ACCT_OBJ_T(); + WATER_USER_CONTRACT_REF_T contractRef = toContractRef(accounting.getWaterUser(), + accounting.getContractName()); + watUsrContractAcctObjT.setWATER_USER_CONTRACT_REF(contractRef); + watUsrContractAcctObjT.setACCOUNTING_REMARKS(transfer.getComment()); + watUsrContractAcctObjT.setPUMP_FLOW(transfer.getFlow()); + LOOKUP_TYPE_OBJ_T transferType = toLookupTypeO(new LookupType.Builder() + .withDisplayValue(transfer.getTransferTypeDisplay()) + .withActive(true) + .withOfficeId(accounting.getWaterUser().getProjectId().getOfficeId()) + .build()); + watUsrContractAcctObjT.setPHYSICAL_TRANSFER_TYPE(transferType); + watUsrContractAcctObjT.setPUMP_LOCATION_REF(getPumpLocationRef(transfer.getPumpType(), pumpIn, pumpOut, pumpBelow)); + watUsrContractAcctObjT.setTRANSFER_START_DATETIME(Timestamp.from(entry.getKey())); + return watUsrContractAcctObjT; + } + + private static LOCATION_REF_T getPumpLocationRef(PumpType pumpType, LOCATION_REF_T pumpIn, LOCATION_REF_T pumpOut, LOCATION_REF_T pumpBelow) { + switch (pumpType) { + case IN: + return pumpIn; + case OUT: + return pumpOut; + case BELOW: + return pumpBelow; + default: + logger.atWarning().log("Invalid pump type"); + throw new IllegalArgumentException( + String.format("Invalid pump type for mapping to DB object: %s", pumpType)); + } + } + static LOC_REF_TIME_WINDOW_TAB_T toTimeWindowTabT(WaterSupplyAccounting accounting) { List timeWindowList = new ArrayList<>(); LOCATION_REF_T pumpIn = LocationUtil.getLocationRef(accounting.getPumpLocations().getPumpIn()); @@ -239,7 +293,7 @@ static LOC_REF_TIME_WINDOW_TAB_T toTimeWindowTabT(WaterSupplyAccounting accounti timeWindow.setLOCATION_REF(pumpBelow); break; default: - LOGGER.atWarning().log("Invalid pump type"); + logger.atWarning().log("Invalid pump type"); break; } timeWindow.setSTART_DATE(Timestamp.from(entry.getKey())); @@ -251,7 +305,7 @@ static LOC_REF_TIME_WINDOW_TAB_T toTimeWindowTabT(WaterSupplyAccounting accounti } static List toWaterSupplyAccountingList(Connection c, WAT_USR_CONTRACT_ACCT_TAB_T - watUsrContractAcctTabT) { + watUsrContractAcctTabT, String flowUnits) { List waterSupplyAccounting = new ArrayList<>(); Map cacheMap = new TreeMap<>(); @@ -269,9 +323,9 @@ static List toWaterSupplyAccountingList(Connection c, WAT .build(); if (cacheMap.containsKey(key)) { WaterSupplyAccounting accounting = cacheMap.get(key); - addTransfer(watUsrContractAcctObjT, accounting); + addTransfer(watUsrContractAcctObjT, accounting, flowUnits); } else { - cacheMap.put(key, createAccounting(c, watUsrContractAcctObjT)); + cacheMap.put(key, createAccounting(c, watUsrContractAcctObjT, flowUnits)); } } for (Map.Entry entry : cacheMap.entrySet()) { @@ -280,7 +334,38 @@ static List toWaterSupplyAccountingList(Connection c, WAT return waterSupplyAccounting; } - private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONTRACT_ACCT_OBJ_T acctObjT) { + // Like the other toWaterSupplyAccountingList but this one takes handgen class. + public static List toWaterSupplyAccountingList(Connection c, cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T + watUsrContractAcctTabT) { + + List waterSupplyAccounting = new ArrayList<>(); + Map cacheMap = new TreeMap<>(); + + for (cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT : watUsrContractAcctTabT) { + WATER_USER_CONTRACT_REF_T watUsrContractRef = watUsrContractAcctObjT.getWATER_USER_CONTRACT_REF(); + WaterSupplyUtils.AccountingKey key = new WaterSupplyUtils.AccountingKey.Builder() + .withContractName(watUsrContractRef.getCONTRACT_NAME()) + .withWaterUser(new WaterUser.Builder() + .withWaterRight(watUsrContractRef.getWATER_USER().getWATER_RIGHT()) + .withEntityName(watUsrContractRef.getWATER_USER().getENTITY_NAME()) + .withProjectId(CwmsId.buildCwmsId(watUsrContractRef.getWATER_USER().getPROJECT_LOCATION_REF().getOFFICE_ID(), + watUsrContractRef.getWATER_USER().getPROJECT_LOCATION_REF().call_GET_LOCATION_ID())) + .build()) + .build(); + if (cacheMap.containsKey(key)) { + WaterSupplyAccounting accounting = cacheMap.get(key); + addTransfer(watUsrContractAcctObjT, accounting, watUsrContractAcctObjT.getPUMP_FLOW_UNIT()); + } else { + cacheMap.put(key, createAccounting(c, watUsrContractAcctObjT, watUsrContractAcctObjT.getPUMP_FLOW_UNIT())); + } + } + for (Map.Entry entry : cacheMap.entrySet()) { + waterSupplyAccounting.add(entry.getValue()); + } + return waterSupplyAccounting; + } + + private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONTRACT_ACCT_OBJ_T acctObjT, String flowUnits) { WaterContractDao waterContractDao = new WaterContractDao(DSL.using(c)); WATER_USER_OBJ_T waterUserObjT = acctObjT.getWATER_USER_CONTRACT_REF().getWATER_USER(); WaterUserContract waterUserContract = waterContractDao.getWaterContract( @@ -307,13 +392,13 @@ private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONT PumpTransfer transfer = null; if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(pumpLocation) && pumpIn.getOfficeId().equalsIgnoreCase(pumpOffice)) { - transfer = new PumpTransfer(PumpType.IN, transferDisplay, flow, remarks); + transfer = new PumpTransfer(PumpType.IN, transferDisplay, flow, flowUnits, remarks); } else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(pumpLocation) && pumpOut.getOfficeId().equalsIgnoreCase(pumpOffice)) { - transfer = new PumpTransfer(PumpType.OUT, transferDisplay, flow, remarks); + transfer = new PumpTransfer(PumpType.OUT, transferDisplay, flow, flowUnits, remarks); } else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(pumpLocation) && pumpBelow.getOfficeId().equalsIgnoreCase(pumpOffice)) { - transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, flow, remarks); + transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, flow, flowUnits, remarks); } if (transfer != null) { pumpAccounting.put(transferStart, Collections.singletonList(transfer)); @@ -330,7 +415,57 @@ private static WaterSupplyAccounting createAccounting(Connection c, WAT_USR_CONT .build(); } - private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupplyAccounting accounting) { + private static WaterSupplyAccounting createAccounting(Connection c, cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T acctObjT, String flowUnits) { + WaterContractDao waterContractDao = new WaterContractDao(DSL.using(c)); + WATER_USER_OBJ_T waterUserObjT = acctObjT.getWATER_USER_CONTRACT_REF().getWATER_USER(); + WaterUserContract waterUserContract = waterContractDao.getWaterContract( + acctObjT.getWATER_USER_CONTRACT_REF().getCONTRACT_NAME(), + new CwmsId.Builder() + .withOfficeId(waterUserObjT.getPROJECT_LOCATION_REF().getOFFICE_ID()) + .withName(waterUserObjT.getPROJECT_LOCATION_REF().call_GET_LOCATION_ID()) + .build(), + waterUserObjT.getENTITY_NAME()); + Map> pumpAccounting = new TreeMap<>(); + String pumpLocation = acctObjT.getPUMP_LOCATION_REF().call_GET_LOCATION_ID(); + String pumpOffice = acctObjT.getPUMP_LOCATION_REF().getOFFICE_ID(); + String transferDisplay = acctObjT.getPHYSICAL_TRANSFER_TYPE().getDISPLAY_VALUE(); + Location pumpIn = waterUserContract.getPumpInLocation() != null + ? waterUserContract.getPumpInLocation().getPumpLocation() : null; + Location pumpOut = waterUserContract.getPumpOutLocation() != null + ? waterUserContract.getPumpOutLocation().getPumpLocation() : null; + Location pumpBelow = waterUserContract.getPumpOutBelowLocation() != null + ? waterUserContract.getPumpOutBelowLocation().getPumpLocation() : null; + Instant transferStart = acctObjT.getTRANSFER_START_DATETIME().toInstant(); + String remarks = acctObjT.getACCOUNTING_REMARKS(); + double flow = acctObjT.getPUMP_FLOW(); + + PumpTransfer transfer = null; + if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(pumpLocation) + && pumpIn.getOfficeId().equalsIgnoreCase(pumpOffice)) { + transfer = new PumpTransfer(PumpType.IN, transferDisplay, flow, flowUnits, remarks); + } else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(pumpLocation) + && pumpOut.getOfficeId().equalsIgnoreCase(pumpOffice)) { + transfer = new PumpTransfer(PumpType.OUT, transferDisplay, flow, flowUnits, remarks); + } else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(pumpLocation) + && pumpBelow.getOfficeId().equalsIgnoreCase(pumpOffice)) { + transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, flow, flowUnits, remarks); + } + if (transfer != null) { + pumpAccounting.put(transferStart, Collections.singletonList(transfer)); + } + return new WaterSupplyAccounting.Builder() + .withContractName(acctObjT.getWATER_USER_CONTRACT_REF().getCONTRACT_NAME()) + .withWaterUser(toWaterUser(waterUserObjT)) + .withPumpLocations(new PumpLocation.Builder() + .withPumpIn(pumpIn != null ? CwmsId.buildCwmsId(pumpIn.getOfficeId(), pumpIn.getName()) : null) + .withPumpOut(pumpOut != null ? CwmsId.buildCwmsId(pumpOut.getOfficeId(), pumpOut.getName()) : null) + .withPumpBelow(pumpBelow != null ? CwmsId.buildCwmsId(pumpBelow.getOfficeId(), pumpBelow.getName()) : null) + .build()) + .withPumpAccounting(pumpAccounting) + .build(); + } + + public static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupplyAccounting accounting, String flowUnits) { PumpTransfer transfer = null; String transferDisplay = acctObjTs.getPHYSICAL_TRANSFER_TYPE().getDISPLAY_VALUE(); String accountingRemarks = acctObjTs.getACCOUNTING_REMARKS(); @@ -343,14 +478,14 @@ private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupp if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(locationId) && pumpIn.getOfficeId().equalsIgnoreCase(officeId)) { - transfer = new PumpTransfer(PumpType.IN, transferDisplay, acctObjTs.getPUMP_FLOW(), accountingRemarks); + transfer = new PumpTransfer(PumpType.IN, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); } else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(locationId) && pumpOut.getOfficeId().equalsIgnoreCase(officeId)) { - transfer = new PumpTransfer(PumpType.OUT, transferDisplay, acctObjTs.getPUMP_FLOW(), accountingRemarks); + transfer = new PumpTransfer(PumpType.OUT, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); } else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(locationId) && pumpBelow.getOfficeId().equalsIgnoreCase(officeId)) { transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, - acctObjTs.getPUMP_FLOW(), accountingRemarks); + acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); } if (accounting.getPumpAccounting().get(transferStart) != null) { List transfers = new ArrayList<>(accounting.getPumpAccounting().get(transferStart)); @@ -362,7 +497,81 @@ private static void addTransfer(WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupp Collections.singletonList(transfer)); } - static class AccountingKey implements Comparable { + public static void addTransfer(cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T acctObjTs, WaterSupplyAccounting accounting, String flowUnits) { + PumpTransfer transfer = null; + String transferDisplay = acctObjTs.getPHYSICAL_TRANSFER_TYPE().getDISPLAY_VALUE(); + String accountingRemarks = acctObjTs.getACCOUNTING_REMARKS(); + Instant transferStart = acctObjTs.getTRANSFER_START_DATETIME().toInstant(); + String officeId = acctObjTs.getPUMP_LOCATION_REF().getOFFICE_ID(); + String locationId = acctObjTs.getPUMP_LOCATION_REF().call_GET_LOCATION_ID(); + CwmsId pumpIn = accounting.getPumpLocations().getPumpIn(); + CwmsId pumpOut = accounting.getPumpLocations().getPumpOut(); + CwmsId pumpBelow = accounting.getPumpLocations().getPumpBelow(); + + if (pumpIn != null && pumpIn.getName().equalsIgnoreCase(locationId) + && pumpIn.getOfficeId().equalsIgnoreCase(officeId)) { + transfer = new PumpTransfer(PumpType.IN, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); + } else if (pumpOut != null && pumpOut.getName().equalsIgnoreCase(locationId) + && pumpOut.getOfficeId().equalsIgnoreCase(officeId)) { + transfer = new PumpTransfer(PumpType.OUT, transferDisplay, acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); + } else if (pumpBelow != null && pumpBelow.getName().equalsIgnoreCase(locationId) + && pumpBelow.getOfficeId().equalsIgnoreCase(officeId)) { + transfer = new PumpTransfer(PumpType.BELOW, transferDisplay, + acctObjTs.getPUMP_FLOW(), flowUnits, accountingRemarks); + } + if (accounting.getPumpAccounting().get(transferStart) != null) { + List transfers = new ArrayList<>(accounting.getPumpAccounting().get(transferStart)); + transfers.add(transfer); + accounting.getPumpAccounting().put(transferStart, transfers); + return; + } + accounting.getPumpAccounting().put(transferStart, + Collections.singletonList(transfer)); + } + + public static cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T toManualWaterUserContractAcctTs(WaterSupplyAccounting accounting) { + List watUsrContractAcctObjTList = new ArrayList<>(); + LOCATION_REF_T pumpIn = LocationUtil.getLocationRef(accounting.getPumpLocations().getPumpIn()); + LOCATION_REF_T pumpOut = LocationUtil.getLocationRef(accounting.getPumpLocations().getPumpOut()); + LOCATION_REF_T pumpBelow = LocationUtil.getLocationRef(accounting.getPumpLocations().getPumpBelow()); + + for (Map.Entry> entry : accounting.getPumpAccounting().entrySet()) { + for (PumpTransfer transfer : entry.getValue()) { + cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT = + getManualWatUsrContractAcctObjT(accounting, entry, transfer, pumpIn, pumpOut, pumpBelow); + watUsrContractAcctObjTList.add(watUsrContractAcctObjT); + } + } + return new cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T(watUsrContractAcctObjTList); + } + + private static cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T getManualWatUsrContractAcctObjT( + WaterSupplyAccounting accounting, + Map.Entry> entry, + PumpTransfer transfer, + LOCATION_REF_T pumpIn, + LOCATION_REF_T pumpOut, + LOCATION_REF_T pumpBelow) { + cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T watUsrContractAcctObjT = + new cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T(); + WATER_USER_CONTRACT_REF_T contractRef = toContractRef(accounting.getWaterUser(), + accounting.getContractName()); + watUsrContractAcctObjT.setWATER_USER_CONTRACT_REF(contractRef); + watUsrContractAcctObjT.setACCOUNTING_REMARKS(transfer.getComment()); + watUsrContractAcctObjT.setPUMP_FLOW(transfer.getFlow()); + watUsrContractAcctObjT.setPUMP_FLOW_UNIT(transfer.getFlowUnit()); + LOOKUP_TYPE_OBJ_T transferType = toLookupTypeO(new LookupType.Builder() + .withDisplayValue(transfer.getTransferTypeDisplay()) + .withActive(true) + .withOfficeId(accounting.getWaterUser().getProjectId().getOfficeId()) + .build()); + watUsrContractAcctObjT.setPHYSICAL_TRANSFER_TYPE(transferType); + watUsrContractAcctObjT.setPUMP_LOCATION_REF(getPumpLocationRef(transfer.getPumpType(), pumpIn, pumpOut, pumpBelow)); + watUsrContractAcctObjT.setTRANSFER_START_DATETIME(Timestamp.from(entry.getKey())); + return watUsrContractAcctObjT; + } + + public static class AccountingKey implements Comparable { private final WaterUser waterUser; private final String contractName; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/CWMS_WATER_SUPPLY_PACKAGE.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/CWMS_WATER_SUPPLY_PACKAGE.java new file mode 100644 index 0000000000..dd7237b693 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/CWMS_WATER_SUPPLY_PACKAGE.java @@ -0,0 +1,43 @@ +package cwms.cda.data.dao.watersupply.handgen; + +import cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T; +import java.math.BigInteger; +import java.sql.Timestamp; +import org.jooq.Configuration; +import usace.cwms.db.jooq.codegen.udt.records.LOC_REF_TIME_WINDOW_TAB_T; +import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; + +public class CWMS_WATER_SUPPLY_PACKAGE { + public static void call_STORE_ACCOUNTING_SET(Configuration configuration, + WAT_USR_CONTRACT_ACCT_TAB_T P_ACCOUNTING_TAB, + WATER_USER_CONTRACT_REF_T P_CONTRACT_REF, + LOC_REF_TIME_WINDOW_TAB_T P_PUMP_TIME_WINDOW_TAB, String P_TIME_ZONE, String P_FLOW_UNIT_ID, String P_STORE_RULE, String P_OVERRIDE_PROT) { + STORE_ACCOUNTING_SET p = new STORE_ACCOUNTING_SET(); + p.setP_ACCOUNTING_TAB(P_ACCOUNTING_TAB); + p.setP_CONTRACT_REF(P_CONTRACT_REF); + p.setP_PUMP_TIME_WINDOW_TAB(P_PUMP_TIME_WINDOW_TAB); + p.setP_TIME_ZONE(P_TIME_ZONE); + p.setP_FLOW_UNIT_ID(P_FLOW_UNIT_ID); + p.setP_STORE_RULE(P_STORE_RULE); + p.setP_OVERRIDE_PROT(P_OVERRIDE_PROT); + p.execute(configuration); + } + + public static WAT_USR_CONTRACT_ACCT_TAB_T call_RETRIEVE_ACCOUNTING_SET(Configuration configuration, + WATER_USER_CONTRACT_REF_T P_CONTRACT_REF, String P_UNITS, Timestamp P_START_TIME, Timestamp P_END_TIME, String P_TIME_ZONE, String P_START_INCLUSIVE, String P_END_INCLUSIVE, String P_ASCENDING_FLAG, BigInteger P_ROW_LIMIT, String P_TRANSFER_TYPE) { + RETRIEVE_ACCOUNTING_SET p = new RETRIEVE_ACCOUNTING_SET(); + p.setP_CONTRACT_REF(P_CONTRACT_REF); + p.setP_UNITS(P_UNITS); + p.setP_START_TIME(P_START_TIME); + p.setP_END_TIME(P_END_TIME); + p.setP_TIME_ZONE(P_TIME_ZONE); + p.setP_START_INCLUSIVE(P_START_INCLUSIVE); + p.setP_END_INCLUSIVE(P_END_INCLUSIVE); + p.setP_ASCENDING_FLAG(P_ASCENDING_FLAG); + p.setP_ROW_LIMIT(P_ROW_LIMIT); + p.setP_TRANSFER_TYPE(P_TRANSFER_TYPE); + p.execute(configuration); + return p.getP_ACCOUNTING_SET(); + } + +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/RETRIEVE_ACCOUNTING_SET.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/RETRIEVE_ACCOUNTING_SET.java new file mode 100644 index 0000000000..e920f87279 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/RETRIEVE_ACCOUNTING_SET.java @@ -0,0 +1,107 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package cwms.cda.data.dao.watersupply.handgen; + +import java.math.BigInteger; +import java.sql.Timestamp; +import org.jooq.Parameter; +import org.jooq.impl.AbstractRoutine; +import org.jooq.impl.DSL; +import org.jooq.impl.DateAsTimestampBinding; +import org.jooq.impl.Internal; +import org.jooq.impl.SQLDataType; +import usace.cwms.db.jooq.codegen.CWMS_20; +import usace.cwms.db.jooq.codegen.packages.CWMS_WATER_SUPPLY_PACKAGE; +import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; +import cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T; + +public class RETRIEVE_ACCOUNTING_SET extends AbstractRoutine { + private static final long serialVersionUID = 1L; + public static final Parameter P_ACCOUNTING_SET; + public static final Parameter P_CONTRACT_REF; + public static final Parameter P_UNITS; + public static final Parameter P_START_TIME; + public static final Parameter P_END_TIME; + public static final Parameter P_TIME_ZONE; + public static final Parameter P_START_INCLUSIVE; + public static final Parameter P_END_INCLUSIVE; + public static final Parameter P_ASCENDING_FLAG; + public static final Parameter P_ROW_LIMIT; + public static final Parameter P_TRANSFER_TYPE; + + public RETRIEVE_ACCOUNTING_SET() { + super("RETRIEVE_ACCOUNTING_SET", CWMS_20.CWMS_20, CWMS_WATER_SUPPLY_PACKAGE.CWMS_WATER_SUPPLY); + this.addOutParameter(P_ACCOUNTING_SET); + this.addInParameter(P_CONTRACT_REF); + this.addInParameter(P_UNITS); + this.addInParameter(P_START_TIME); + this.addInParameter(P_END_TIME); + this.addInParameter(P_TIME_ZONE); + this.addInParameter(P_START_INCLUSIVE); + this.addInParameter(P_END_INCLUSIVE); + this.addInParameter(P_ASCENDING_FLAG); + this.addInParameter(P_ROW_LIMIT); + this.addInParameter(P_TRANSFER_TYPE); + } + + public void setP_CONTRACT_REF(WATER_USER_CONTRACT_REF_T value) { + this.setValue(P_CONTRACT_REF, value); + } + + public void setP_UNITS(String value) { + this.setValue(P_UNITS, value); + } + + public void setP_START_TIME(Timestamp value) { + this.setValue(P_START_TIME, value); + } + + public void setP_END_TIME(Timestamp value) { + this.setValue(P_END_TIME, value); + } + + public void setP_TIME_ZONE(String value) { + this.setValue(P_TIME_ZONE, value); + } + + public void setP_START_INCLUSIVE(String value) { + this.setValue(P_START_INCLUSIVE, value); + } + + public void setP_END_INCLUSIVE(String value) { + this.setValue(P_END_INCLUSIVE, value); + } + + public void setP_ASCENDING_FLAG(String value) { + this.setValue(P_ASCENDING_FLAG, value); + } + + public void setP_ROW_LIMIT(BigInteger value) { + this.setValue(P_ROW_LIMIT, value); + } + + public void setP_TRANSFER_TYPE(String value) { + this.setValue(P_TRANSFER_TYPE, value); + } + + public WAT_USR_CONTRACT_ACCT_TAB_T getP_ACCOUNTING_SET() { + return (WAT_USR_CONTRACT_ACCT_TAB_T)this.get(P_ACCOUNTING_SET); + } + + static { + P_ACCOUNTING_SET = Internal.createParameter("P_ACCOUNTING_SET", WAT_USR_CONTRACT_ACCT_OBJ_T.WAT_USR_CONTRACT_ACCT_OBJ_T.getDataType().asArrayDataType(WAT_USR_CONTRACT_ACCT_TAB_T.class), false, false); + P_CONTRACT_REF = Internal.createParameter("P_CONTRACT_REF", usace.cwms.db.jooq.codegen.udt.WATER_USER_CONTRACT_REF_T.WATER_USER_CONTRACT_REF_T.getDataType(), false, false); + P_UNITS = Internal.createParameter("P_UNITS", SQLDataType.VARCHAR, false, false); + P_START_TIME = Internal.createParameter("P_START_TIME", SQLDataType.TIMESTAMP(0), false, false, new DateAsTimestampBinding()); + P_END_TIME = Internal.createParameter("P_END_TIME", SQLDataType.TIMESTAMP(0), false, false, new DateAsTimestampBinding()); + P_TIME_ZONE = Internal.createParameter("P_TIME_ZONE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_START_INCLUSIVE = Internal.createParameter("P_START_INCLUSIVE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_END_INCLUSIVE = Internal.createParameter("P_END_INCLUSIVE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_ASCENDING_FLAG = Internal.createParameter("P_ASCENDING_FLAG", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_ROW_LIMIT = Internal.createParameter("P_ROW_LIMIT", SQLDataType.DECIMAL_INTEGER(38).defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.DECIMAL_INTEGER)), true, false); + P_TRANSFER_TYPE = Internal.createParameter("P_TRANSFER_TYPE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/STORE_ACCOUNTING_SET.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/STORE_ACCOUNTING_SET.java new file mode 100644 index 0000000000..6e2d25fc75 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/STORE_ACCOUNTING_SET.java @@ -0,0 +1,79 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package cwms.cda.data.dao.watersupply.handgen; + +import org.jooq.Parameter; +import org.jooq.impl.AbstractRoutine; +import org.jooq.impl.DSL; +import org.jooq.impl.Internal; +import org.jooq.impl.SQLDataType; +import usace.cwms.db.jooq.codegen.CWMS_20; +import usace.cwms.db.jooq.codegen.packages.CWMS_WATER_SUPPLY_PACKAGE; +import usace.cwms.db.jooq.codegen.udt.LOC_REF_TIME_WINDOW_OBJ_T; +import usace.cwms.db.jooq.codegen.udt.WAT_USR_CONTRACT_ACCT_OBJ_T; +import usace.cwms.db.jooq.codegen.udt.records.LOC_REF_TIME_WINDOW_TAB_T; +import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; +import cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_TAB_T; + +public class STORE_ACCOUNTING_SET extends AbstractRoutine { + private static final long serialVersionUID = 1L; + public static final Parameter P_ACCOUNTING_TAB; + public static final Parameter P_CONTRACT_REF; + public static final Parameter P_PUMP_TIME_WINDOW_TAB; + public static final Parameter P_TIME_ZONE; + public static final Parameter P_FLOW_UNIT_ID; + public static final Parameter P_STORE_RULE; + public static final Parameter P_OVERRIDE_PROT; + + public STORE_ACCOUNTING_SET() { + super("STORE_ACCOUNTING_SET", CWMS_20.CWMS_20, CWMS_WATER_SUPPLY_PACKAGE.CWMS_WATER_SUPPLY); + this.addInParameter(P_ACCOUNTING_TAB); + this.addInParameter(P_CONTRACT_REF); + this.addInParameter(P_PUMP_TIME_WINDOW_TAB); + this.addInParameter(P_TIME_ZONE); + this.addInParameter(P_FLOW_UNIT_ID); + this.addInParameter(P_STORE_RULE); + this.addInParameter(P_OVERRIDE_PROT); + } + + public void setP_ACCOUNTING_TAB(WAT_USR_CONTRACT_ACCT_TAB_T value) { + this.setValue(P_ACCOUNTING_TAB, value); + } + + public void setP_CONTRACT_REF(WATER_USER_CONTRACT_REF_T value) { + this.setValue(P_CONTRACT_REF, value); + } + + public void setP_PUMP_TIME_WINDOW_TAB(LOC_REF_TIME_WINDOW_TAB_T value) { + this.setValue(P_PUMP_TIME_WINDOW_TAB, value); + } + + public void setP_TIME_ZONE(String value) { + this.setValue(P_TIME_ZONE, value); + } + + public void setP_FLOW_UNIT_ID(String value) { + this.setValue(P_FLOW_UNIT_ID, value); + } + + public void setP_STORE_RULE(String value) { + this.setValue(P_STORE_RULE, value); + } + + public void setP_OVERRIDE_PROT(String value) { + this.setValue(P_OVERRIDE_PROT, value); + } + + static { + P_ACCOUNTING_TAB = Internal.createParameter("P_ACCOUNTING_TAB", WAT_USR_CONTRACT_ACCT_OBJ_T.WAT_USR_CONTRACT_ACCT_OBJ_T.getDataType().asArrayDataType(WAT_USR_CONTRACT_ACCT_TAB_T.class), false, false); + P_CONTRACT_REF = Internal.createParameter("P_CONTRACT_REF", usace.cwms.db.jooq.codegen.udt.WATER_USER_CONTRACT_REF_T.WATER_USER_CONTRACT_REF_T.getDataType(), false, false); + P_PUMP_TIME_WINDOW_TAB = Internal.createParameter("P_PUMP_TIME_WINDOW_TAB", LOC_REF_TIME_WINDOW_OBJ_T.LOC_REF_TIME_WINDOW_OBJ_T.getDataType().asArrayDataType(LOC_REF_TIME_WINDOW_TAB_T.class), false, false); + P_TIME_ZONE = Internal.createParameter("P_TIME_ZONE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_FLOW_UNIT_ID = Internal.createParameter("P_FLOW_UNIT_ID", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_STORE_RULE = Internal.createParameter("P_STORE_RULE", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + P_OVERRIDE_PROT = Internal.createParameter("P_OVERRIDE_PROT", SQLDataType.VARCHAR.defaultValue(DSL.field(DSL.raw("NULL"), SQLDataType.VARCHAR)), true, false); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/WAT_USR_CONTRACT_ACCT_OBJ_T.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/WAT_USR_CONTRACT_ACCT_OBJ_T.java new file mode 100644 index 0000000000..32c70ada60 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/WAT_USR_CONTRACT_ACCT_OBJ_T.java @@ -0,0 +1,54 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package cwms.cda.data.dao.watersupply.handgen; + +import java.sql.Timestamp; +import org.jooq.Package; +import org.jooq.Schema; +import org.jooq.UDTField; +import org.jooq.impl.DSL; +import org.jooq.impl.DateAsTimestampBinding; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.SchemaImpl; +import org.jooq.impl.UDTImpl; +import usace.cwms.db.jooq.codegen.CWMS_20; +import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; +import usace.cwms.db.jooq.codegen.udt.records.LOOKUP_TYPE_OBJ_T; +import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; + +public class WAT_USR_CONTRACT_ACCT_OBJ_T extends UDTImpl { + private static final long serialVersionUID = 1L; + public static final WAT_USR_CONTRACT_ACCT_OBJ_T WAT_USR_CONTRACT_ACCT_OBJ_T = new WAT_USR_CONTRACT_ACCT_OBJ_T(); + public static final UDTField WATER_USER_CONTRACT_REF; + public static final UDTField PUMP_LOCATION_REF; + public static final UDTField PHYSICAL_TRANSFER_TYPE; + public static final UDTField PUMP_FLOW; + public static final UDTField PUMP_FLOW_UNIT; + public static final UDTField TRANSFER_START_DATETIME; + public static final UDTField ACCOUNTING_REMARKS; + + public Class getRecordType() { + return cwms.cda.data.dao.watersupply.handgen.records.WAT_USR_CONTRACT_ACCT_OBJ_T.class; + } + + WAT_USR_CONTRACT_ACCT_OBJ_T() { + super("WAT_USR_CONTRACT_ACCT_OBJ_T", (Schema)null, (Package)null, false); + } + + public Schema getSchema() { + return (Schema)(CWMS_20.CWMS_20 != null ? CWMS_20.CWMS_20 : new SchemaImpl(DSL.name("CWMS_20"))); + } + + static { + WATER_USER_CONTRACT_REF = createField(DSL.name("WATER_USER_CONTRACT_REF"), usace.cwms.db.jooq.codegen.udt.WATER_USER_CONTRACT_REF_T.WATER_USER_CONTRACT_REF_T.getDataType(), WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + PUMP_LOCATION_REF = createField(DSL.name("PUMP_LOCATION_REF"), usace.cwms.db.jooq.codegen.udt.LOCATION_REF_T.LOCATION_REF_T.getDataType(), WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + PHYSICAL_TRANSFER_TYPE = createField(DSL.name("PHYSICAL_TRANSFER_TYPE"), usace.cwms.db.jooq.codegen.udt.LOOKUP_TYPE_OBJ_T.LOOKUP_TYPE_OBJ_T.getDataType(), WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + PUMP_FLOW = createField(DSL.name("PUMP_FLOW"), SQLDataType.DOUBLE, WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + PUMP_FLOW_UNIT = createField(DSL.name("PUMP_FLOW_UNIT"), SQLDataType.VARCHAR(16), WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + TRANSFER_START_DATETIME = createField(DSL.name("TRANSFER_START_DATETIME"), SQLDataType.TIMESTAMP(0), WAT_USR_CONTRACT_ACCT_OBJ_T, "", new DateAsTimestampBinding()); + ACCOUNTING_REMARKS = createField(DSL.name("ACCOUNTING_REMARKS"), SQLDataType.VARCHAR(255), WAT_USR_CONTRACT_ACCT_OBJ_T, ""); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_OBJ_T.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_OBJ_T.java new file mode 100644 index 0000000000..4964936cd1 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_OBJ_T.java @@ -0,0 +1,236 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package cwms.cda.data.dao.watersupply.handgen.records; + +import java.sql.Timestamp; +import org.jooq.Field; +import org.jooq.Record7; +import org.jooq.Row7; +import org.jooq.impl.UDTRecordImpl; +import usace.cwms.db.jooq.codegen.udt.records.LOCATION_REF_T; +import usace.cwms.db.jooq.codegen.udt.records.LOOKUP_TYPE_OBJ_T; +import usace.cwms.db.jooq.codegen.udt.records.WATER_USER_CONTRACT_REF_T; + +public class WAT_USR_CONTRACT_ACCT_OBJ_T extends UDTRecordImpl + implements Record7 { + private static final long serialVersionUID = 1L; + + public WAT_USR_CONTRACT_ACCT_OBJ_T setWATER_USER_CONTRACT_REF(WATER_USER_CONTRACT_REF_T value) { + this.set(0, value); + return this; + } + + public WATER_USER_CONTRACT_REF_T getWATER_USER_CONTRACT_REF() { + return (WATER_USER_CONTRACT_REF_T)this.get(0); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setPUMP_LOCATION_REF(LOCATION_REF_T value) { + this.set(1, value); + return this; + } + + public LOCATION_REF_T getPUMP_LOCATION_REF() { + return (LOCATION_REF_T)this.get(1); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setPHYSICAL_TRANSFER_TYPE(LOOKUP_TYPE_OBJ_T value) { + this.set(2, value); + return this; + } + + public LOOKUP_TYPE_OBJ_T getPHYSICAL_TRANSFER_TYPE() { + return (LOOKUP_TYPE_OBJ_T)this.get(2); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setPUMP_FLOW(Double value) { + this.set(3, value); + return this; + } + + public Double getPUMP_FLOW() { + return (Double)this.get(3); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setPUMP_FLOW_UNIT(String value) { + this.set(4, value); + return this; + } + + public String getPUMP_FLOW_UNIT() { + return (String)this.get(4); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setTRANSFER_START_DATETIME(Timestamp value) { + this.set(5, value); + return this; + } + + public Timestamp getTRANSFER_START_DATETIME() { + return (Timestamp)this.get(5); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T setACCOUNTING_REMARKS(String value) { + this.set(6, value); + return this; + } + + public String getACCOUNTING_REMARKS() { + return (String)this.get(6); + } + + public Row7 fieldsRow() { + return (Row7)super.fieldsRow(); + } + + public Row7 valuesRow() { + return (Row7)super.valuesRow(); + } + + public Field field1() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.WATER_USER_CONTRACT_REF; + } + + public Field field2() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.PUMP_LOCATION_REF; + } + + public Field field3() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.PHYSICAL_TRANSFER_TYPE; + } + + public Field field4() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.PUMP_FLOW; + } + + public Field field5() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.PUMP_FLOW_UNIT; + } + public Field field6() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.TRANSFER_START_DATETIME; + } + + public Field field7() { + return cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.ACCOUNTING_REMARKS; + } + + public WATER_USER_CONTRACT_REF_T component1() { + return this.getWATER_USER_CONTRACT_REF(); + } + + public LOCATION_REF_T component2() { + return this.getPUMP_LOCATION_REF(); + } + + public LOOKUP_TYPE_OBJ_T component3() { + return this.getPHYSICAL_TRANSFER_TYPE(); + } + + public Double component4() { + return this.getPUMP_FLOW(); + } + + public String component5() { + return this.getPUMP_FLOW_UNIT(); + } + + public Timestamp component6() { + return this.getTRANSFER_START_DATETIME(); + } + + public String component7() { + return this.getACCOUNTING_REMARKS(); + } + + public WATER_USER_CONTRACT_REF_T value1() { + return this.getWATER_USER_CONTRACT_REF(); + } + + public LOCATION_REF_T value2() { + return this.getPUMP_LOCATION_REF(); + } + + public LOOKUP_TYPE_OBJ_T value3() { + return this.getPHYSICAL_TRANSFER_TYPE(); + } + + public Double value4() { + return this.getPUMP_FLOW(); + } + + public String value5() { + return this.getPUMP_FLOW_UNIT(); + } + + public Timestamp value6() { + return this.getTRANSFER_START_DATETIME(); + } + + public String value7() { + return this.getACCOUNTING_REMARKS(); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value1(WATER_USER_CONTRACT_REF_T value) { + this.setWATER_USER_CONTRACT_REF(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value2(LOCATION_REF_T value) { + this.setPUMP_LOCATION_REF(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value3(LOOKUP_TYPE_OBJ_T value) { + this.setPHYSICAL_TRANSFER_TYPE(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value4(Double value) { + this.setPUMP_FLOW(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value5(String value) { + this.setPUMP_FLOW_UNIT(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value6(Timestamp value) { + this.setTRANSFER_START_DATETIME(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T value7(String value) { + this.setACCOUNTING_REMARKS(value); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T values(WATER_USER_CONTRACT_REF_T value1, LOCATION_REF_T value2, LOOKUP_TYPE_OBJ_T value3, Double value4, String value5, Timestamp value6, String value7) { + this.value1(value1); + this.value2(value2); + this.value3(value3); + this.value4(value4); + this.value5(value5); + this.value6(value6); + this.value7(value7); + return this; + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T() { + super(cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.WAT_USR_CONTRACT_ACCT_OBJ_T); + } + + public WAT_USR_CONTRACT_ACCT_OBJ_T(WATER_USER_CONTRACT_REF_T WATER_USER_CONTRACT_REF, LOCATION_REF_T PUMP_LOCATION_REF, LOOKUP_TYPE_OBJ_T PHYSICAL_TRANSFER_TYPE, Double PUMP_FLOW, String PUMP_FLOW_UNIT, Timestamp TRANSFER_START_DATETIME, String ACCOUNTING_REMARKS) { + super(cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.WAT_USR_CONTRACT_ACCT_OBJ_T); + this.setWATER_USER_CONTRACT_REF(WATER_USER_CONTRACT_REF); + this.setPUMP_LOCATION_REF(PUMP_LOCATION_REF); + this.setPHYSICAL_TRANSFER_TYPE(PHYSICAL_TRANSFER_TYPE); + this.setPUMP_FLOW(PUMP_FLOW); + this.setPUMP_FLOW_UNIT(PUMP_FLOW_UNIT); + this.setTRANSFER_START_DATETIME(TRANSFER_START_DATETIME); + this.setACCOUNTING_REMARKS(ACCOUNTING_REMARKS); + this.resetChangedOnNotNull(); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_TAB_T.java b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_TAB_T.java new file mode 100644 index 0000000000..6d622c68a3 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dao/watersupply/handgen/records/WAT_USR_CONTRACT_ACCT_TAB_T.java @@ -0,0 +1,32 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by Fernflower decompiler) +// + +package cwms.cda.data.dao.watersupply.handgen.records; + +import java.util.Arrays; +import java.util.Collection; +import org.jooq.impl.ArrayRecordImpl; +import usace.cwms.db.jooq.codegen.CWMS_20; + +public class WAT_USR_CONTRACT_ACCT_TAB_T extends ArrayRecordImpl { + private static final long serialVersionUID = 1L; + + public WAT_USR_CONTRACT_ACCT_TAB_T() { + super(CWMS_20.CWMS_20, "WAT_USR_CONTRACT_ACCT_TAB_T", cwms.cda.data.dao.watersupply.handgen.WAT_USR_CONTRACT_ACCT_OBJ_T.WAT_USR_CONTRACT_ACCT_OBJ_T.getDataType()); + } + + public WAT_USR_CONTRACT_ACCT_TAB_T(WAT_USR_CONTRACT_ACCT_OBJ_T... array) { + this(); + if (array != null) { + this.addAll(Arrays.asList(array)); + } + + } + + public WAT_USR_CONTRACT_ACCT_TAB_T(Collection collection) { + this(); + this.addAll(collection); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/AssignedTimeSeries.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/AssignedTimeSeries.java index 6dc58aa0ae..85d140cef8 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/AssignedTimeSeries.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/AssignedTimeSeries.java @@ -24,7 +24,10 @@ package cwms.cda.data.dto; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public class AssignedTimeSeries extends CwmsDTOBase { private String officeId; private String timeseriesId; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsDTOPaginated.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsDTOPaginated.java index d27229872e..08c959148f 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsDTOPaginated.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/CwmsDTOPaginated.java @@ -1,12 +1,15 @@ package cwms.cda.data.dto; import com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Base64; import java.util.Base64.Decoder; import java.util.Base64.Encoder; import com.google.common.flogger.FluentLogger; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -132,6 +135,21 @@ public static String[] decodeCursor(String cursor, String delimiter) { return new String[0]; } + public static Optional decodeCursor(String cursor, Class clazz) { + return decodeCursor(cursor, CwmsDTOPaginated.delimiter, clazz); + } + + public static Optional decodeCursor(String cursor, String delimiter, Class clazz) { + try { + T typed = clazz.getDeclaredConstructor().newInstance(); + typed.decodeCursor(cursor, delimiter); + return Optional.of(typed); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + logger.atInfo().withCause(e).log("Failed to instantiate cursor class %s", clazz.getName()); + } + return Optional.empty(); + } + public static String encodeCursor(String page, int pageSize) { return encodeCursor(CwmsDTOPaginated.delimiter, page, pageSize); } @@ -150,6 +168,14 @@ public static String encodeCursor(String delimiter, Object ... parts) encoder.encodeToString(Arrays.stream(parts).map(String::valueOf).collect(Collectors.joining(delimiter)).getBytes()); } + public static String encodeCursor(PageCursor pageCursor) { + return encodeCursor(CwmsDTOPaginated.delimiter, pageCursor); + } + + public static String encodeCursor(String delimiter, PageCursor cursor) { + return cursor.encode(encoder, delimiter); + } + public static class CursorCheck implements Function1 { private static Pattern base64 = Pattern.compile("^[-A-Za-z0-9+/]*={0,3}$"); @Override diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/PageCursor.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/PageCursor.java new file mode 100644 index 0000000000..eec73c6b0d --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/PageCursor.java @@ -0,0 +1,38 @@ +package cwms.cda.data.dto; + +import java.util.Base64.Encoder; + +public interface PageCursor { + /** + * Decodes the provided cursor string using the specified delimiter and sets the appropriate fields in the implementing class. + * @param cursor the encoded cursor string to decode + * @param delimiter the delimiter used to separate fields in the encoded cursor string. By default, this is || + */ + void decodeCursor(String cursor, String delimiter); + + /** + * Encodes the fields of the implementing class into a cursor string using the specified encoder and delimiter. + * @param encoder the Base64 Encoder to use for encoding the cursor string + * @param delimiter the delimiter used to separate fields in the encoded cursor string. By default, this is || + * @return the encoded cursor string representing the current state of the implementing class's fields + */ + String encode(Encoder encoder, String delimiter); + + /** + * Encodes a field that may be null into a string representation. If the field is null, it returns the string "null". Otherwise, it returns the string representation of the field. + * @param field the field to encode, which may be null + * @return a string representation of the field, where null is represented as "null" + */ + static String encodeNullableField(Object field) { + return field == null ? "null" : field.toString(); + } + + /** + * Decodes a string representation of a field that may be null. If the input string is "null", it returns null. Otherwise, it returns the input string as is. + * @param field the string representation of the field to decode, where "null" represents a null value + * @return the decoded field, which is null if the input string is "null", or the input string itself if it is not "null". The caller is responsible for converting the string to the appropriate type if necessary. + */ + static String decodeNullableField(String field) { + return "null".equals(field) ? null : field; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/ParameterLegacy.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/ParameterLegacy.java new file mode 100644 index 0000000000..967b4a1ffd --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/ParameterLegacy.java @@ -0,0 +1,130 @@ +package cwms.cda.data.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; + +/** + * DTO used exclusively for legacy JSON (version 1) parameter payloads. + * The property names follow kebab-case to match the legacy API fields. + */ +@FormattableWith(contentType = Formats.JSON_LEGACY, formatter = JsonV1.class, aliases = {Formats.DEFAULT, Formats.JSON, Formats.JSONV1}) +@JsonRootName("parameter") +@JsonDeserialize(builder = ParameterLegacy.Builder.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +public final class ParameterLegacy extends CwmsDTOBase { + + private final String abstractParam; + private final String name; + private final String office; + private final String defaultEnglishUnit; + private final String defaultSiUnit; + private final String longName; + private final String description; + + private ParameterLegacy(Builder builder) { + this.abstractParam = builder.abstractParam; + this.name = builder.name; + this.office = builder.office; + this.defaultEnglishUnit = builder.defaultEnglishUnit; + this.defaultSiUnit = builder.defaultSiUnit; + this.longName = builder.longName; + this.description = builder.description; + } + + public String getAbstractParam() { + return abstractParam; + } + + public String getName() { + return name; + } + + public String getOffice() { + return office; + } + + public String getDefaultEnglishUnit() { + return defaultEnglishUnit; + } + + public String getDefaultSiUnit() { + return defaultSiUnit; + } + + public String getLongName() { + return longName; + } + + public String getDescription() { + return description; + } + + public static class Builder { + private String abstractParam; + private String name; + private String office; + private String defaultEnglishUnit; + private String defaultSiUnit; + private String longName; + private String description; + + public Builder withAbstractParam(String abstractParam) { + this.abstractParam = abstractParam; + return this; + } + + public Builder withName(String name) { + this.name = name; + return this; + } + + public Builder withOffice(String office) { + this.office = office; + return this; + } + + public Builder withDefaultEnglishUnit(String defaultEnglishUnit) { + this.defaultEnglishUnit = defaultEnglishUnit; + return this; + } + + public Builder withDefaultSiUnit(String defaultSiUnit) { + this.defaultSiUnit = defaultSiUnit; + return this; + } + + public Builder withLongName(String longName) { + this.longName = longName; + return this; + } + + public Builder withDescription(String description) { + this.description = description; + return this; + } + + @JsonIgnore + public Builder from(ParameterLegacy parameter) { + this.abstractParam = parameter.getAbstractParam(); + this.name = parameter.getName(); + this.office = parameter.getOffice(); + this.defaultEnglishUnit = parameter.getDefaultEnglishUnit(); + this.defaultSiUnit = parameter.getDefaultSiUnit(); + this.longName = parameter.getLongName(); + this.description = parameter.getDescription(); + return this; + } + + public ParameterLegacy build() { + return new ParameterLegacy(this); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java index e4369a3cbe..004d3604c9 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/VerticalDatumInfo.java @@ -7,20 +7,36 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dao.VerticalDatum; +import cwms.cda.formatters.Formats; +import cwms.cda.formatters.annotations.FormattableWith; +import cwms.cda.formatters.json.JsonV1; +import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv1; +import cwms.cda.formatters.xml.XMLv2; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @JsonRootName("vertical-datum-info") @JsonDeserialize(builder = VerticalDatumInfo.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) +@FormattableWith(contentType = Formats.XMLV1, formatter = XMLv1.class) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class, aliases = {Formats.XML}) +@FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class) public class VerticalDatumInfo extends CwmsDTOBase { - + @JacksonXmlProperty(isAttribute = true) String office; + @JacksonXmlProperty(isAttribute = true) String unit; String location; @@ -31,6 +47,8 @@ public class VerticalDatumInfo extends CwmsDTOBase { // Serialize empty arrays in the xml @JsonInclude(JsonInclude.Include.ALWAYS) + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "offset") VerticalDatumInfo.Offset[] offsets = new Offset[0]; private VerticalDatumInfo() { @@ -100,13 +118,24 @@ private VerticalDatumInfo.Offset[] buildConvertedOffsets(VerticalDatum convertTo //add the other offsets, adjusted VerticalDatumInfo.Offset[] offsets = getOffsets(); + //if contains a zero offset, we will mimic that for the converted datum by adding a zero offset (the datum we converted to) + boolean hasZeroOffset = Arrays.stream(offsets) + .anyMatch(offset -> offset.getValue() == 0.0); for (VerticalDatumInfo.Offset offset : offsets) { String toDatum = offset.getToDatum(); - if (!offset.isForDatum(convertTo.toString())) { - Double newOffsetValue = convertToOffsetToOriginal + offset.getValue(); - boolean isEstimate = offset.isEstimate() || convertToOffset.isEstimate(); - VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(isEstimate, toDatum, newOffsetValue); - newOffsets.add(newOffset); + Set existingDatums = newOffsets.stream().map(Offset::getToDatum) + .collect(Collectors.toSet()); + if(!existingDatums.contains(offset.getToDatum())) { + if (!offset.isForDatum(convertTo.toString())) { + Double newOffsetValue = convertToOffsetToOriginal + offset.getValue(); + boolean isEstimate = offset.isEstimate() || convertToOffset.isEstimate(); + VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(isEstimate, toDatum, newOffsetValue); + newOffsets.add(newOffset); + } else if(hasZeroOffset) { + //this is the one we converted to, its now zero offset + VerticalDatumInfo.Offset newOffset = new VerticalDatumInfo.Offset(false, toDatum, 0.0); + newOffsets.add(newOffset); + } } } return newOffsets.toArray(new VerticalDatumInfo.Offset[]{}); @@ -114,6 +143,7 @@ private VerticalDatumInfo.Offset[] buildConvertedOffsets(VerticalDatum convertTo @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static class Offset { + @JacksonXmlProperty(isAttribute = true) boolean estimate; String toDatum; @@ -231,6 +261,8 @@ public VerticalDatumInfo.Builder withElevation(Double elevation) { return this; } + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "offset") public VerticalDatumInfo.Builder withOffsets(VerticalDatumInfo.Offset[] offsets) { this.offsets = offsets; return this; @@ -248,13 +280,15 @@ public VerticalDatumInfo.Builder withLocalDatumName(String localDatumName) { @JsonIgnore public Builder from(VerticalDatumInfo vdi) { - this.office = vdi.getOffice(); - this.unit = vdi.getUnit(); - this.location = vdi.getLocation(); - this.nativeDatum = vdi.getNativeDatum(); - this.elevation = vdi.getElevation(); - this.offsets = vdi.getOffsets(); - this.localDatumName = vdi.getLocalDatumName(); + if(vdi != null) { + this.office = vdi.getOffice(); + this.unit = vdi.getUnit(); + this.location = vdi.getLocation(); + this.nativeDatum = vdi.getNativeDatum(); + this.elevation = vdi.getElevation(); + this.offsets = vdi.getOffsets(); + this.localDatumName = vdi.getLocalDatumName(); + } return this; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/Users.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/Users.java index 1ea7b72996..76da03ca3e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/Users.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/Users.java @@ -13,11 +13,9 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import cwms.cda.data.dto.CwmsDTO; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV1; -import cwms.cda.formatters.json.JsonV2; import io.swagger.v3.oas.annotations.media.Schema; import cwms.cda.formatters.Formats; @@ -55,12 +53,14 @@ public static class Builder { private final Users workingUsers; private final Optional nextPage; private final String limitOffice; + private final String userNameRegex; - public Builder(String cursor, int pageSize, int total, String limitOffice) { + public Builder(String cursor, int pageSize, int total, String limitOffice, String userNameRegex) { workingUsers = new Users(cursor, pageSize, total); this.nextPage = Optional.empty(); this.limitOffice = limitOffice; + this.userNameRegex = userNameRegex; } /** @@ -78,6 +78,7 @@ public Builder(@JsonProperty("page") String cursor, workingUsers = new Users(cursor, pageSize, total); this.nextPage = Optional.of(nextPage != null ? nextPage : "end"); this.limitOffice = null; // Not used when processing existing JSON, value is encoded in next-page for the query. + this.userNameRegex = null; // Not used when processing existing JSON, value is encoded in next-page for the query. } public Users build() { @@ -87,7 +88,12 @@ public Users build() { } else if (this.workingUsers.users.size() == this.workingUsers.pageSize && !this.workingUsers.users.isEmpty()) { User lastUser = this.workingUsers.users.get(this.workingUsers.users.size() - 1); - this.workingUsers.nextPage = encodeCursor(CwmsDTOPaginated.delimiter, lastUser.getUserName(), this.workingUsers.pageSize, this.workingUsers.total, this.limitOffice); + UsersPageCursor pageCursor = new UsersPageCursor.Builder(lastUser.getUserName(), this.workingUsers.pageSize, this.workingUsers.total) + .withLimitOffice(this.limitOffice) + .withUsernameRegex(this.userNameRegex) + .build(); + this.workingUsers.nextPage = CwmsDTOPaginated.encodeCursor(pageCursor); + } else { this.workingUsers.nextPage = null; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/UsersPageCursor.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/UsersPageCursor.java new file mode 100644 index 0000000000..d7d588d5af --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/auth/users/UsersPageCursor.java @@ -0,0 +1,114 @@ +package cwms.cda.data.dto.auth.users; + +import cwms.cda.data.dto.CwmsDTOPaginated; +import cwms.cda.data.dto.PageCursor; + +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; + +import static cwms.cda.data.dto.PageCursor.decodeNullableField; +import static cwms.cda.data.dto.PageCursor.encodeNullableField; + +public final class UsersPageCursor implements PageCursor { + private static final int CURSOR_USER_ID_INDEX = 0; + private static final int PAGE_SIZE_INDEX = 1; + private static final int TOTAL_INDEX = 2; + private static final int LIMIT_OFFICE_INDEX = 3; + private static final int USERNAME_REGEX_INDEX = 4; + private static final int EXPECTED_CURSOR_PARTS = 5; + String cursorUserId; + int total; + int pageSize; + String limitOffice; + String usernameRegex; + + public UsersPageCursor() { + } + + private UsersPageCursor(Builder builder) { + this.cursorUserId = builder.cursorUserId; + this.total = builder.total; + this.pageSize = builder.pageSize; + this.limitOffice = builder.limitOffice; + this.usernameRegex = builder.usernameRegex; + } + + public String getCursorUserId() { + return cursorUserId; + } + + public int getTotal() { + return total; + } + + public int getPageSize() { + return pageSize; + } + + public String getLimitOffice() { + return limitOffice; + } + + public String getUsernameRegex() { + return usernameRegex; + } + + @Override + public void decodeCursor(String cursor, String delimiter) { + String[] decoded = CwmsDTOPaginated.decodeCursor(cursor, delimiter); + if(decoded.length == EXPECTED_CURSOR_PARTS) { + cursorUserId = decoded[CURSOR_USER_ID_INDEX]; + pageSize = Integer.parseInt(decoded[PAGE_SIZE_INDEX]); + total = Integer.parseInt(decoded[TOTAL_INDEX]); + limitOffice = decodeNullableField(decoded[LIMIT_OFFICE_INDEX]); + usernameRegex = decodeNullableField(decoded[USERNAME_REGEX_INDEX]); + } else { + throw new IllegalArgumentException("Invalid cursor format"); + } + } + + @Override + public String encode(Base64.Encoder encoder, String delimiter) { + Object[] cursorParts = new Object[EXPECTED_CURSOR_PARTS]; + cursorParts[CURSOR_USER_ID_INDEX] = cursorUserId; + cursorParts[PAGE_SIZE_INDEX] = pageSize; + cursorParts[TOTAL_INDEX] = total; + cursorParts[LIMIT_OFFICE_INDEX] = encodeNullableField(limitOffice); + cursorParts[USERNAME_REGEX_INDEX] = encodeNullableField(usernameRegex); + return (cursorUserId == null || cursorUserId.equals("*")) ? null : + encoder.encodeToString(Arrays.stream(cursorParts) + .map(String::valueOf) + .collect(Collectors.joining(delimiter)) + .getBytes()); + + } + + public static class Builder { + private final String cursorUserId; + private final int total; + private final int pageSize; + private String limitOffice; + private String usernameRegex; + + public Builder(String cursorUserId, int pageSize, int total) { + this.cursorUserId = cursorUserId; + this.pageSize = pageSize; + this.total = total; + } + + public Builder withLimitOffice(String limitOffice) { + this.limitOffice = limitOffice; + return this; + } + + public Builder withUsernameRegex(String usernameRegex) { + this.usernameRegex = usernameRegex; + return this; + } + + public UsersPageCursor build() { + return new UsersPageCursor(this); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java index 036d9f0428..f68ef1a87e 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpec.java @@ -1,10 +1,17 @@ package cwms.cda.data.dto.rating; +import cwms.cda.data.dto.CwmsDTOBase; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlText; -public class IndependentRoundingSpec { +@JsonDeserialize(using = IndependentRoundingSpecDeserializer.class) +public class IndependentRoundingSpec extends CwmsDTOBase { + @JacksonXmlProperty(isAttribute = true) private final Integer position; + @JacksonXmlText private final String value; public IndependentRoundingSpec(@JsonProperty("position") Integer position, @JsonProperty( diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java new file mode 100644 index 0000000000..96752b14d6 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/IndependentRoundingSpecDeserializer.java @@ -0,0 +1,50 @@ +package cwms.cda.data.dto.rating; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; + +import java.io.IOException; + +public class IndependentRoundingSpecDeserializer extends JsonDeserializer { + @Override + public IndependentRoundingSpec deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Integer position = null; + String value = null; + + if (p instanceof FromXmlParser) { + FromXmlParser xmlP = (FromXmlParser) p; + if (xmlP.getCurrentToken() == JsonToken.START_OBJECT || xmlP.getCurrentToken() == JsonToken.FIELD_NAME) { + try { + String posAttr = xmlP.getStaxReader().getAttributeValue(null, "position"); + if (posAttr != null) { + position = Integer.parseInt(posAttr); + } + } catch (IllegalStateException e) { + // Not at START_ELEMENT, ignore + } + } + } + + if (p.getCurrentToken() == JsonToken.START_OBJECT) { + while (p.nextToken() != JsonToken.END_OBJECT) { + String fieldName = p.getCurrentName(); + p.nextToken(); + if ("position".equals(fieldName)) { + String posStr = p.getValueAsString(); + if (posStr != null) { + position = Integer.parseInt(posStr); + } + } else if ("value".equals(fieldName) || "".equals(fieldName)) { + value = p.getText(); + } + } + } else if (p.getCurrentToken() == JsonToken.VALUE_STRING) { + value = p.getText(); + } + + return new IndependentRoundingSpec(position, value); + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java index 8d46542200..fbe1923185 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpec.java @@ -2,15 +2,18 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import cwms.cda.api.errors.FieldException; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dto.CwmsDTO; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import hec.data.cwmsRating.RatingConst; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -18,10 +21,12 @@ import java.util.List; import org.jetbrains.annotations.NotNull; +@JsonRootName("rating-spec") @JsonDeserialize(builder = RatingSpec.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class) public class RatingSpec extends CwmsDTO { private final String ratingId; private final String templateId; @@ -36,6 +41,7 @@ public class RatingSpec extends CwmsDTO { private final boolean autoUpdate; private final boolean autoActivate; private final boolean autoMigrateExtension; + private final IndependentRoundingSpec[] independentRoundingSpecs; private final String dependentRoundingSpec; private final String description; @@ -112,6 +118,8 @@ public boolean isAutoMigrateExtension() { return autoMigrateExtension; } + @JacksonXmlElementWrapper(localName = "independent-rounding-specs") + @JacksonXmlProperty(localName = "independent-rounding-spec") public IndependentRoundingSpec[] getIndependentRoundingSpecs() { return independentRoundingSpecs; } @@ -175,7 +183,6 @@ public boolean equals(Object o) { that.getSourceAgency()) : that.getSourceAgency() != null) { return false; } - // Probably incorrect - comparing Object[] arrays with Arrays.equals if (!Arrays.equals(getIndependentRoundingSpecs(), that.getIndependentRoundingSpecs())) { return false; } @@ -319,6 +326,7 @@ public Builder withIndependentRoundingSpecs(IndependentRoundingSpec[] indRoundin return this; } + public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( String indRoundingSpecsStr) { IndependentRoundingSpec[] retval = null; @@ -330,7 +338,7 @@ public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( } @NotNull - private static IndependentRoundingSpec[] buildIndependentRoundingSpecs( + public static IndependentRoundingSpec[] buildIndependentRoundingSpecs( String[] indRoundingSpecsStrArr) { IndependentRoundingSpec[] retval; retval = new IndependentRoundingSpec[indRoundingSpecsStrArr.length]; diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java index 5a568e0a42..9685033b52 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/rating/RatingSpecs.java @@ -1,47 +1,109 @@ package cwms.cda.data.dto.rating; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonRootName; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import cwms.cda.data.dto.CwmsDTOPaginated; import cwms.cda.formatters.Formats; import cwms.cda.formatters.annotations.FormattableWith; import cwms.cda.formatters.json.JsonV2; +import cwms.cda.formatters.xml.XMLv2; import java.util.ArrayList; import java.util.Collections; import java.util.List; +@JsonRootName("rating-specs") @JsonDeserialize(builder = RatingSpecs.Builder.class) @JsonInclude(JsonInclude.Include.NON_NULL) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) @FormattableWith(contentType = Formats.JSONV2, formatter = JsonV2.class, aliases = {Formats.DEFAULT, Formats.JSON}) +@FormattableWith(contentType = Formats.XMLV2, formatter = XMLv2.class) public class RatingSpecs extends CwmsDTOPaginated { private List specs; private RatingSpecs() { - } - private int offset; - private RatingSpecs(int offset, int pageSize, Integer total, List specsList) { super(Integer.toString(offset), pageSize, total); specs = new ArrayList<>(specsList); - this.offset = offset; } + @JacksonXmlElementWrapper(localName = "specs") + @JacksonXmlProperty(localName = "rating-spec") public List getSpecs() { return Collections.unmodifiableList(specs); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RatingSpecs that = (RatingSpecs) o; + + if (getPageSize() != that.getPageSize()) return false; + if (getSpecs() != null ? !getSpecs().equals(that.getSpecs()) : that.getSpecs() != null) return false; + if (getPage() != null ? !getPage().equals(that.getPage()) : that.getPage() != null) return false; + if (getNextPage() != null ? !getNextPage().equals(that.getNextPage()) : that.getNextPage() != null) + return false; + return getTotal() != null ? getTotal().equals(that.getTotal()) : that.getTotal() == null; + } + + @Override + public int hashCode() { + int result = getSpecs() != null ? getSpecs().hashCode() : 0; + result = 31 * result + (getPage() != null ? getPage().hashCode() : 0); + result = 31 * result + (getNextPage() != null ? getNextPage().hashCode() : 0); + result = 31 * result + (getTotal() != null ? getTotal().hashCode() : 0); + result = 31 * result + getPageSize(); + return result; + } + + @JsonPOJOBuilder + @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public static class Builder { - private final int offset; - private final int pageSize; - private final Integer total; - private List specs; + private int offset; + private int pageSize; + private Integer total; + private List specs = new ArrayList<>(); + + public Builder() { + } + + public Builder withPage(String page) { + String[] parts = decodeCursor(page); + if (parts.length > 0) { + try { + this.offset = Integer.parseInt(parts[0]); + } catch (NumberFormatException e) { + // Try different delimiter if default fails, though decodeCursor should handle it if base64 + } + } + return this; + } + + public Builder withOffset(int offset) { + this.offset = offset; + return this; + } + + public Builder withPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + public Builder withTotal(Integer total) { + this.total = total; + return this; + } public Builder(int offset, int pageSize, Integer total) { this.offset = offset; @@ -49,7 +111,7 @@ public Builder(int offset, int pageSize, Integer total) { this.total = total; } - public Builder specs(List specList) { + public Builder withSpecs(List specList) { this.specs = specList; return this; } @@ -57,8 +119,8 @@ public Builder specs(List specList) { public RatingSpecs build() { RatingSpecs retval = new RatingSpecs(offset, pageSize, total, specs); - if (this.specs.size() == this.pageSize) { - String cursor = Integer.toString(retval.offset + retval.specs.size()); + if (this.specs != null && this.specs.size() == this.pageSize) { + String cursor = Integer.toString(offset + retval.specs.size()); retval.nextPage = encodeCursor(cursor, retval.pageSize, retval.total); diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/PumpTransfer.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/PumpTransfer.java index 019f904320..dab7f1facf 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/PumpTransfer.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/PumpTransfer.java @@ -2,7 +2,7 @@ * * MIT License * - * Copyright (c) 2024 Hydrologic Engineering Center + * Copyright (c) 2026 Hydrologic Engineering Center * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -41,14 +41,17 @@ public final class PumpTransfer extends CwmsDTOBase { @JsonProperty(required = true) private final Double flow; @JsonProperty(required = true) + private final String flowUnit; + @JsonProperty(required = true) private final String comment; @JsonCreator public PumpTransfer(@JsonProperty("pump-type") PumpType pumpType, - @JsonProperty("transfer-type-display") String transferTypeDisplay, - @JsonProperty("flow") Double flow, @JsonProperty("comment") String comment) { + @JsonProperty("transfer-type-display") String transferTypeDisplay, + @JsonProperty("flow") Double flow, @JsonProperty("flow-unit") String flowUnit, @JsonProperty("comment") String comment) { this.transferTypeDisplay = transferTypeDisplay; this.flow = flow; + this.flowUnit = flowUnit; this.comment = comment; this.pumpType = pumpType; } @@ -61,6 +64,10 @@ public Double getFlow() { return this.flow; } + public String getFlowUnit() { + return this.flowUnit; + } + public PumpType getPumpType() { return this.pumpType; } diff --git a/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/WaterSupplyAccounting.java b/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/WaterSupplyAccounting.java index 3421c4ecb0..2aaeb52495 100644 --- a/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/WaterSupplyAccounting.java +++ b/cwms-data-api/src/main/java/cwms/cda/data/dto/watersupply/WaterSupplyAccounting.java @@ -40,7 +40,7 @@ import java.util.Map; @FormattableWith(contentType = Formats.JSONV1, formatter = JsonV1.class, - aliases = {Formats.DEFAULT, Formats.JSON}) + aliases = {Formats.DEFAULT, Formats.JSON, Formats.JSONV2}) @JsonDeserialize(builder = WaterSupplyAccounting.Builder.class) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy.class) public final class WaterSupplyAccounting extends CwmsDTOBase { diff --git a/cwms-data-api/src/main/java/cwms/cda/datasource/LrtsSessionPreparer.java b/cwms-data-api/src/main/java/cwms/cda/datasource/LrtsSessionPreparer.java new file mode 100644 index 0000000000..0342cccfc9 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/datasource/LrtsSessionPreparer.java @@ -0,0 +1,100 @@ +package cwms.cda.datasource; + +import static cwms.cda.data.dao.Dao.formatBool; +import static org.jooq.SQLDialect.ORACLE18C; + +import com.google.common.flogger.FluentLogger; +import cwms.cda.data.dao.JooqDao; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.Connection; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; + + +/** + * Prepares a connection by setting the LRTS session flag. + */ +public class LrtsSessionPreparer implements ConnectionPreparer { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Boolean isNewLrts; + private final boolean clearOnClose; + + public LrtsSessionPreparer(Boolean isNewLrts) { + this(isNewLrts, false); + } + + public LrtsSessionPreparer(Boolean isNewLrts, boolean clear) { + this.isNewLrts = isNewLrts; + clearOnClose = clear; + } + + @Override + public Connection prepare(Connection connection) { + if (isNewLrts == null) { + return connection; + } + + DSLContext dsl = DSL.using(connection, ORACLE18C); + + // Can also get current value and remember it and then reset to that in the close + // if setting with null,null doesn't work. + // GET_SESSION_INFO sessionInfo = CWMS_UTIL_PACKAGE.call_GET_SESSION_INFO(dslContext.configuration() + // , SESSION_USE_LRTS_ID_FORMAT); + + String requireBool = formatBool(isNewLrts); + int requireIntValue = isNewLrts ? JooqDao.REQUIRE_NEW_LRTS_ID_FORMAT + : JooqDao.REQUIRE_OLD_LRTS_ID_FORMAT; + CWMS_UTIL_PACKAGE.call_SET_SESSION_INFO(dsl.configuration(), + JooqDao.SESSION_USE_LRTS_ID_FORMAT, requireBool, requireIntValue); + logger.atFine().log("Set LRTS session flag to %s (%d) for connection %s", + requireBool, requireIntValue, connection); + + if (clearOnClose) { + // Return a proxy that will unset the flag on close() + return (Connection) Proxy.newProxyInstance( + connection.getClass().getClassLoader(), + new Class[]{Connection.class}, + new CloseUnsettingHandler(connection)); + } else { + return connection; + } + + } + + private static class CloseUnsettingHandler implements InvocationHandler { + private final Connection delegate; + private volatile boolean closed = false; + + CloseUnsettingHandler(Connection delegate) { + this.delegate = delegate; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String name = method.getName(); + if ("close".equals(name)) { + if (!closed) { + try { + DSLContext dsl = DSL.using(delegate, ORACLE18C); + // Passing null values to clear the session setting + CWMS_UTIL_PACKAGE.call_SET_SESSION_INFO(dsl.configuration(), + JooqDao.SESSION_USE_LRTS_ID_FORMAT, null, null); + logger.atFine().log("Cleared LRTS session flag for connection %s", delegate); + } catch (RuntimeException ex) { + logger.atWarning().withCause(ex) + .log("Failed to clear LRTS session flag on connection close"); + } finally { + closed = true; + } + } + return method.invoke(delegate, args); + } + return method.invoke(delegate, args); + } + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatureManagerProvider.java b/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatureManagerProvider.java new file mode 100644 index 0000000000..a952a77b81 --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatureManagerProvider.java @@ -0,0 +1,36 @@ +package cwms.cda.features; + +import com.google.auto.service.AutoService; +import org.togglz.core.manager.FeatureManager; +import org.togglz.core.manager.FeatureManagerBuilder; +import org.togglz.core.repository.file.FileBasedStateRepository; +import java.io.File; +import org.togglz.core.spi.FeatureManagerProvider; + +@AutoService(FeatureManagerProvider.class) +public class CdaFeatureManagerProvider implements FeatureManagerProvider { + public static final String DEFAULT_PROPERTIES_FILE = "features.properties"; + public static final String PROPERTIES_FILE = "properties.file"; + private volatile FeatureManager manager; + + @Override + public int priority() { + return 10; + } + + @Override + public FeatureManager getFeatureManager() { + if (manager == null) { + synchronized (this) { + if (manager == null) { + String file = System.getProperty(PROPERTIES_FILE, DEFAULT_PROPERTIES_FILE); + manager = new FeatureManagerBuilder() + .featureEnum(CdaFeatures.class) + .stateRepository(new FileBasedStateRepository(new File(file))) + .build(); + } + } + } + return manager; + } +} diff --git a/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatures.java b/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatures.java new file mode 100644 index 0000000000..fa2a662efa --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/features/CdaFeatures.java @@ -0,0 +1,9 @@ +package cwms.cda.features; + +import org.togglz.core.Feature; +import org.togglz.core.annotation.Label; + +public enum CdaFeatures implements Feature { + @Label("Use object-storage backed Blob DAO in BlobController") + USE_OBJECT_STORAGE_BLOBS +} diff --git a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java index a28dd01fa3..fc70b68242 100644 --- a/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java +++ b/cwms-data-api/src/main/java/cwms/cda/formatters/xml/XMLv2.java @@ -82,7 +82,7 @@ public T parseContent(InputStream content, Class type } } - private static @NotNull XmlMapper buildXmlMapper() { + public static @NotNull XmlMapper buildXmlMapper() { XmlMapper retval = new XmlMapper(); retval.findAndRegisterModules(); // Without these two disables an Instant gets written as 3333333.335000000 diff --git a/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java b/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java new file mode 100644 index 0000000000..cd06733e1b --- /dev/null +++ b/cwms-data-api/src/main/java/cwms/cda/helpers/annotations/IgnoreRequiredQueryParamMismatch.java @@ -0,0 +1,51 @@ +/* + * + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.helpers.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * Inform the OpenAPI documentation verification process to ignore mismatches + * in required query parameters between the annotated element and the OpenAPI specification. + * This is useful in cases where the implementation intentionally deviates from the specification + * for certain parameters, such as when a parameter is one of a set of mutually exclusive required parameters. + */ +@Documented +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreRequiredQueryParamMismatch { + /** + * Returns the names of the parameters that are being ignored in the required parameter mismatch check. + * + * @return the names of the parameters to be ignored + */ + String[] parameterNames(); +} diff --git a/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java b/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java index 8bc146a238..3e967aed0b 100644 --- a/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java +++ b/cwms-data-api/src/main/java/cwms/cda/security/OpenIDConfig.java @@ -2,52 +2,34 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; -import io.swagger.v3.oas.models.security.OAuthFlow; -import io.swagger.v3.oas.models.security.OAuthFlows; -import io.swagger.v3.oas.models.security.Scopes; import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.security.SecurityScheme.In; import io.swagger.v3.oas.models.security.SecurityScheme.Type; public class OpenIDConfig { private static final FluentLogger log = FluentLogger.forEnclosingClass(); - private static final String ALT_WELL_KNOWN = "cwms.dataapi.access.openid.useAltWellKnown"; - private static final boolean USE_ALT_WELLKNOWN; - - static { - String altWellKnownStr = System.getProperty(ALT_WELL_KNOWN,System.getenv(ALT_WELL_KNOWN)); - if (altWellKnownStr != null) { - USE_ALT_WELLKNOWN = Boolean.parseBoolean(altWellKnownStr); - } else { - USE_ALT_WELLKNOWN = false; - } - } - + private URL wellKnown; - private URL altWellKnown = null; // silly, but needed by the docker-compose setup so URLs match and work. + private String issuer; - private URL authUrl; - private URL tokenUrl; - private URL userInfoUrl; - private URL logoutUrl; + private String client_id; + private String idp_hint; // keycloak specific kc_idp_hint to direct federation + private URL jwksUrl; - private Scopes scopes = new Scopes(); - private OAuthFlows flows = new OAuthFlows(); - public OpenIDConfig(URL wellKnown, String altAuthUrl) throws IOException { + public OpenIDConfig(URL wellKnown, String client_id, String idp_hint) throws IOException { this.wellKnown = wellKnown; - if (USE_ALT_WELLKNOWN) { - this.altWellKnown = substituteBase(wellKnown, altAuthUrl); - } - + this.idp_hint = idp_hint; + this.client_id = client_id; HttpURLConnection http = null; try { @@ -60,34 +42,6 @@ public OpenIDConfig(URL wellKnown, String altAuthUrl) throws IOException { JsonNode node = mapper.readTree(http.getInputStream()); jwksUrl = new URL(node.get("jwks_uri").asText()); issuer = node.get("issuer").asText(); - tokenUrl = substituteBase(new URL(node.get("token_endpoint").asText()),altAuthUrl); - userInfoUrl = substituteBase(new URL(node.get("userinfo_endpoint").asText()),altAuthUrl); - logoutUrl = substituteBase(new URL(node.get("end_session_endpoint").asText()),altAuthUrl); - authUrl = substituteBase(new URL(node.get("authorization_endpoint").asText()),altAuthUrl); - JsonNode scopes = node.get("scopes_supported"); - for(JsonNode scope: scopes) { - this.scopes.addString(scope.asText(), ""); - } - - JsonNode grants = node.get("grant_types_supported"); - for(JsonNode grant: grants) { - OAuthFlow flow = new OAuthFlow(); - flow.setTokenUrl(tokenUrl.toString()); - flow.setAuthorizationUrl(authUrl.toString()); - flow.setScopes(this.scopes); - String grantStr = grant.asText(); - if (grantStr.equalsIgnoreCase("implicit")) { - flows.setImplicit(flow); - } else if(grantStr.equalsIgnoreCase("password")) { - flows.setPassword(flow); - } else if(grantStr.equalsIgnoreCase("authorization_code")) { - flows.setAuthorizationCode(flow); - } else if (grantStr.equalsIgnoreCase("client_credentials")) { - flows.setClientCredentials(flow); - } - } - - } else { log.atSevere().log("Unable to retrieve data from realm. Response code %d",status); } @@ -97,30 +51,30 @@ public OpenIDConfig(URL wellKnown, String altAuthUrl) throws IOException { } } } - - private URL substituteBase(URL endPoint, String altAuthUrl) throws MalformedURLException { - if (altAuthUrl == null) { - return endPoint; - } - log.atInfo().log("Changing '%s' with '%s'", endPoint.toString(), altAuthUrl); - String originalPath = endPoint.getPath(); - log.atInfo().log("New Path = %s", altAuthUrl+originalPath); - return new URL(altAuthUrl+originalPath); - } - + public URL getJwksUrl() { return jwksUrl; } public SecurityScheme getScheme() { - URL theUrl = wellKnown; - if (USE_ALT_WELLKNOWN) { - theUrl = altWellKnown; + + + SecurityScheme scheme = new SecurityScheme().type(Type.OPENIDCONNECT) + .openIdConnectUrl(wellKnown.toString()) + .scheme("openid"); + if (idp_hint != null) + { + Map hint = new HashMap<>(); + hint.put("query-parameter", "kc_idp_hint"); + ArrayList values = new ArrayList<>(); + for (String value: idp_hint.split(",")) { + values.add(value.trim()); + } + hint.put("values", values); + scheme.addExtension("x-kc_idp_hint", hint); } - return new SecurityScheme().type(Type.OPENIDCONNECT) - .openIdConnectUrl(theUrl.toString()) - .name("Authorization") - .flows(flows) - .in(In.HEADER); + + scheme.addExtension("x-oidc-client-id", client_id); + return scheme; } } diff --git a/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java b/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java index 2b357b5746..7287c8aa88 100644 --- a/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java +++ b/cwms-data-api/src/main/java/cwms/cda/security/OpenIdConnectIdentitityProvider.java @@ -45,7 +45,8 @@ public final class OpenIdConnectIdentitityProvider implements IdentityProvider { private static final FluentLogger log = FluentLogger.forEnclosingClass(); public static final String WELL_KNOWN_PROPERTY = "cwms.dataapi.access.openid.wellKnownUrl"; - public static final String ALT_AUTH_URL = "cwms.dataapi.access.openid.altAuthUrl"; + public static final String CLIENT_ID = "cwms.dataapi.access.openid.clientId"; + public static final String IDP_HINT = "cwms.dataapi.access.openid.idpHint"; public static final String ISSUER_PROPERTY = "cwms.dataapi.access.openid.issuer"; public static final String TIMEOUT_PROPERTY = "cwms.dataapi.access.openid.timeout"; public static final String AUTHORIZATION = "Authorization"; @@ -64,7 +65,8 @@ public OpenIdConnectIdentitityProvider() { String wellKnownUrl = System.getProperty(WELL_KNOWN_PROPERTY,System.getenv(WELL_KNOWN_PROPERTY)); String issuer = System.getProperty(ISSUER_PROPERTY,System.getenv(ISSUER_PROPERTY)); String timeoutStr = System.getProperty(TIMEOUT_PROPERTY,System.getenv(TIMEOUT_PROPERTY)); - String altAuthUrl = System.getProperty(ALT_AUTH_URL, System.getenv(ALT_AUTH_URL)); + String clientId = System.getProperty(CLIENT_ID, System.getenv(CLIENT_ID)); + String idpHint = System.getProperty(IDP_HINT, System.getenv(IDP_HINT)); int timeout = 3600; if (timeoutStr != null && !timeoutStr.isEmpty()) { timeout = Integer.parseInt(timeoutStr); @@ -73,7 +75,7 @@ public OpenIdConnectIdentitityProvider() { if (wellKnownUrl == null || wellKnownUrl.isEmpty()) { throw new IOException("OpenID Connect well-known URL is not set."); } - config = new OpenIDConfig(new URL(wellKnownUrl), altAuthUrl); + config = new OpenIDConfig(new URL(wellKnownUrl), clientId, idpHint); jwtParser = Jwts.parserBuilder() .requireIssuer(issuer) .setSigningKeyResolver(new UrlResolver(config.getJwksUrl(),timeout)) diff --git a/cwms-data-api/src/main/resources/features.properties b/cwms-data-api/src/main/resources/features.properties new file mode 100644 index 0000000000..94eeb15567 --- /dev/null +++ b/cwms-data-api/src/main/resources/features.properties @@ -0,0 +1 @@ +USE_OBJECT_STORAGE_BLOBS=false \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/api/BaseLineTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/BaseLineTestIT.java index 3436037ea7..bad5737e43 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/BaseLineTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/BaseLineTestIT.java @@ -35,9 +35,6 @@ void test_options_handling_known_url(String url) throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)); - // not setting the headers as WAF is not correctly overwriting them - //.header("Access-Control-Allow-Methods", equalTo("GET, POST, PUT, DELETE, OPTIONS")) - //.header("Access-Control-Allow-Headers", equalTo("Content-Type, Authorization")); } @ParameterizedTest @@ -52,8 +49,6 @@ void test_options_handling_unknown_url(String url) throws Exception { .then() .log().ifValidationFails(LogDetail.ALL,true) .assertThat() - .statusCode(is(HttpServletResponse.SC_OK)) - .header("Access-Control-Allow-Methods", nullValue()) - .header("Access-Control-Allow-Headers", nullValue()); + .statusCode(is(HttpServletResponse.SC_OK)); } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/BinaryTimeSeriesControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/BinaryTimeSeriesControllerTestIT.java index ea7f521cf5..1fe51a1c71 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/BinaryTimeSeriesControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/BinaryTimeSeriesControllerTestIT.java @@ -1,28 +1,28 @@ package cwms.cda.api; +import static cwms.cda.api.BinaryTimeSeriesController.REPLACE_ALL; +import static cwms.cda.api.Controllers.BLOB_ID; +import static cwms.cda.api.Controllers.VERSION_DATE; +import static io.restassured.RestAssured.given; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + import com.fasterxml.jackson.databind.ObjectMapper; import cwms.cda.ApiServlet; import cwms.cda.data.dto.binarytimeseries.BinaryTimeSeries; import cwms.cda.data.dto.binarytimeseries.BinaryTimeSeriesRow; import cwms.cda.formatters.Formats; import cwms.cda.formatters.json.JsonV2; -import cwms.cda.helpers.DatabaseHelpers; import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; -import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import io.restassured.response.ResponseBody; -import mil.army.usace.hec.test.database.CwmsDatabaseContainer; - -import org.apache.commons.io.IOUtils; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URIBuilder; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; - -import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -32,18 +32,19 @@ import java.util.Map; import java.util.Objects; import java.util.Random; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.io.IOUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static cwms.cda.api.BinaryTimeSeriesController.REPLACE_ALL; -import static cwms.cda.api.Controllers.BLOB_ID; -import static cwms.cda.api.Controllers.VERSION_DATE; -import static io.restassured.RestAssured.given; -import static java.util.stream.Collectors.toMap; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - @Tag("integration") final class BinaryTimeSeriesControllerTestIT extends DataApiTestIT { private static final String OFFICE = "SPK"; @@ -61,6 +62,28 @@ static void create() throws Exception { random.nextBytes(LARGE_BYTES); } + @AfterEach + void tearDown() throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(Controllers.OFFICE, OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(Controllers.BEGIN, BEGIN_STR) + .queryParam(Controllers.END, END_STR) + .queryParam(Controllers.OFFICE, OFFICE) + .delete("/timeseries/binary/" + tsId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + @BeforeEach void setup() throws Exception { tsId = locationId + ".Flow.Inst.1Hour.0." + Instant.now().getEpochSecond() + (int)(Math.random() * 100); @@ -141,6 +164,24 @@ void test_get_create_get(String format) throws IOException { .body("binary-values.size()", equalTo(3)) ; + // Delete the binary time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .header("Authorization", user.toHeaderValue()) + .queryParam(Controllers.OFFICE, OFFICE) + .when() + .redirects().follow(true) + .redirects().max(3) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(Controllers.BEGIN, BEGIN_STR) + .queryParam(Controllers.END, END_STR) + .queryParam(Controllers.OFFICE, OFFICE) + .delete("/timeseries/binary/" + tsId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } @@ -522,6 +563,7 @@ void test_create_get_delete_get_lrts(String format) throws Exception { given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, tsIdentifier) .queryParam(Controllers.BEGIN, BEGIN_STR) @@ -548,7 +590,7 @@ void test_create_get_update_get(String format) throws IOException { // 2)Create the binary time series // 3)Retrieve the binary time series and assert that it exists // 4)Update the binary time series - // 5)Retrieve the binary time series and assert that it does not exist + // 5)Retrieve the binary time series and assert that value has updated // Step 1) @@ -640,7 +682,7 @@ void test_create_get_update_get(String format) throws IOException { String newValue = "bmV3VmFsdWU="; // Step 5) - // Retrieve the binary time series and assert that it does not exist + // Retrieve the binary time series and assert that it has new value given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) @@ -659,6 +701,7 @@ void test_create_get_update_get(String format) throws IOException { .body("binary-values.size()", equalTo(3)) .body("binary-values[1].binary-value", equalTo(newValue)) ; + } @NotNull @@ -790,7 +833,6 @@ void test_large_data_url(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .header("Transfer-Encoding", equalTo("chunked")) .contentType(equalTo("application/octet-stream")) .extract() .response() diff --git a/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerObjectStorageTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerObjectStorageTestIT.java new file mode 100644 index 0000000000..b54b8d0817 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerObjectStorageTestIT.java @@ -0,0 +1,52 @@ +package cwms.cda.api; + +import cwms.cda.features.CdaFeatures; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.togglz.core.context.FeatureContext; +import org.togglz.core.manager.FeatureManager; + +@Tag("integration") +@ExtendWith(BlobControllerObjectStorageTestIT.FeatureEnableExtension.class) +public class BlobControllerObjectStorageTestIT extends BlobControllerTestIT { + + static class FeatureEnableExtension implements Extension, BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + + setObjectStoreProperties(); + } + } + + static boolean wasActive; + + private static void setObjectStoreProperties() { + // This test needs the object store feature enabled. + // + // So we make sure its enabled before the test and then restore the feature to however it was after + FeatureManager featureManager = FeatureContext.getFeatureManager(); + + wasActive = featureManager.isActive(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + featureManager.enable(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + featureManager.isActive(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + } + + @AfterAll + public static void teardown() { + // restore the object store feature to however it initially was set. + FeatureManager featureManager = FeatureContext.getFeatureManager(); + if (wasActive) { + featureManager.enable(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + } else { + featureManager.disable(CdaFeatures.USE_OBJECT_STORAGE_BLOBS); + } + + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerTestIT.java index 446e9e9bda..952bab67d3 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/BlobControllerTestIT.java @@ -18,6 +18,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.time.Duration; import static io.restassured.RestAssured.given; @@ -36,6 +37,11 @@ public class BlobControllerTestIT extends DataApiTestIT { private static final String EXISTING_BLOB_VALUE = "test value"; @BeforeAll + public static void setup() throws Exception { + createExistingBlob(); + } + + static void createExistingBlob() throws Exception { String origDesc = "test description"; @@ -67,7 +73,7 @@ static void createExistingBlob() throws Exception @Test void test_getOne_not_found() throws UnsupportedEncodingException { String blobId = "TEST"; - String urlencoded = URLEncoder.encode(blobId, "UTF-8"); + String urlencoded = URLEncoder.encode(blobId, StandardCharsets.UTF_8); given() .log().ifValidationFails(LogDetail.ALL,true) @@ -123,6 +129,35 @@ void test_blob_get_one_default() void test_blob_range() { // We can now do Range requests! + // Our example blob above has a value "test value" + + // If we ask for byte 0 to the _end_ that is like a normal request for the whole file, we should get "test value" + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .header("Range", " bytes=0-") + .when() + .get("/blobs/" + EXISTING_BLOB_ID) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is(EXISTING_BLOB_VALUE)); + + // Our test value is 10 bytes so if we ask for 0-9 we should get the whole value + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .header("Range", " bytes=0-9") + .when() + .get("/blobs/" + EXISTING_BLOB_ID) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is(EXISTING_BLOB_VALUE)); + + // If we ask for byte 3 to the end we should get "t value" given() .log().ifValidationFails(LogDetail.ALL, true) .queryParam(Controllers.OFFICE, SPK) @@ -134,6 +169,19 @@ void test_blob_range() .assertThat() .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) .body( is("t value")); + + // If we ask for byte 3 to 7 we should get "t val" + given() + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .header("Range", " bytes=3-7") + .when() + .get("/blobs/" + EXISTING_BLOB_ID) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is("t val")); } @Test @@ -406,7 +454,7 @@ void test_pagination_works() { nextPage = pageN.path("next-page"); int pageTotal = pageN.path("blobs.size()"); - assertTrue(pageTotal <= pageSize, "Expected the page to return no more than the configured page size"); + assertTrue(pageTotal <= pageSize, "Expected the page to return no more than the configured page size. Expected " + pageTotal + "<=" + pageSize); totalRetrieved += pageTotal; } while( nextPage != null ); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ClobControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/ClobControllerTestIT.java index 654743c7d0..f638d94e4f 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ClobControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ClobControllerTestIT.java @@ -13,6 +13,7 @@ import io.restassured.filter.log.LogDetail; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.RandomStringUtils; @@ -107,7 +108,7 @@ void test_failIfExists() throws Exception { @Test void test_getOne_notFound() throws UnsupportedEncodingException { String clobId = "TEST"; - String urlencoded = URLEncoder.encode(clobId, "UTF-8"); + String urlencoded = URLEncoder.encode(clobId, StandardCharsets.UTF_8); given() .log().ifValidationFails(LogDetail.ALL, true) @@ -150,19 +151,68 @@ of the object name (NOTE: good candidate for actually having a GUID or other "co void test_getOne_plainText_withRange() { // We can now do Range requests! + + // If we ask for byte 0 to the _end_ that is like a normal request for the whole file, we should get "test value" given() - .accept("text/plain") - .log().ifValidationFails(LogDetail.ALL, true) - .queryParam(Controllers.OFFICE, SPK) - .queryParam(Controllers.CLOB_ID, EXISTING_CLOB_ID) - .header("Range"," bytes=3-") - .when() - .get("/clobs/ignored") - .then() - .log().ifValidationFails(LogDetail.ALL, true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) - .body( is("t value")); + .accept("text/plain") + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .queryParam(Controllers.CLOB_ID, EXISTING_CLOB_ID) + .header("Range", " bytes=0-") + .when() + .get("/clobs/ignored") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is(EXISTING_CLOB_VALUE)); + + // Our test value is 10 bytes so if we ask for 0-9 we should get the whole value + given() + .accept("text/plain") + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .queryParam(Controllers.CLOB_ID, EXISTING_CLOB_ID) + .header("Range", " bytes=0-9") + .when() + .get("/clobs/ignored") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is(EXISTING_CLOB_VALUE)); + + // If we ask for byte 3 to the end we should get "t value" + given() + .accept("text/plain") + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .queryParam(Controllers.CLOB_ID, EXISTING_CLOB_ID) + .header("Range", " bytes=3-") + .when() + .get("/clobs/ignored") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is("t value")); + + // If we ask for byte 3 to 7 we should get "t val" + given() + .accept("text/plain") + .log().ifValidationFails(LogDetail.ALL, true) + .queryParam(Controllers.OFFICE, SPK) + .queryParam(Controllers.CLOB_ID, EXISTING_CLOB_ID) + .header("Range", " bytes=3-7") + .when() + .get("/clobs/ignored") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_PARTIAL_CONTENT)) + .body( is("t val")); + + } @Test diff --git a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java index 4311d13940..27cd0cd8a3 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/DataApiTestIT.java @@ -32,6 +32,7 @@ import cwms.cda.data.dao.DeleteRule; import cwms.cda.data.dao.StreamDao; +import cwms.cda.data.dao.VerticalDatum; import cwms.cda.data.dao.basin.BasinDao; import cwms.cda.data.dto.Location; import cwms.cda.data.dto.LocationCategory; @@ -42,9 +43,12 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.IntegrationTestNameGenerator; import fixtures.KeyCloakExtension; +import fixtures.MinIOExtension; import fixtures.TestAccounts; import fixtures.users.MockCwmsUserPrincipalImpl; - +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; import java.io.File; import java.io.IOException; import java.io.StringWriter; @@ -52,7 +56,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; +import java.sql.Date; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; @@ -60,17 +66,12 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; - import mil.army.usace.hec.test.database.CwmsDatabaseContainer; import org.apache.catalina.Manager; import org.apache.catalina.SessionEvent; import org.apache.catalina.SessionListener; import org.apache.catalina.session.StandardSession; import org.apache.commons.io.IOUtils; -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; - import org.jooq.DSLContext; import org.jooq.SQLDialect; import org.jooq.impl.DSL; @@ -81,6 +82,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; +import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; import usace.cwms.db.jooq.codegen.packages.CWMS_UTIL_PACKAGE; /** @@ -90,9 +92,10 @@ @DisplayNameGeneration(IntegrationTestNameGenerator.class) @Tag("integration") @ExtendWith(KeyCloakExtension.class) +@ExtendWith(MinIOExtension.class) @ExtendWith(CwmsDataApiSetupCallback.class) public class DataApiTestIT { - private static FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); protected static String createLocationQuery = null; protected static String createTimeseriesQuery = null; @@ -228,7 +231,7 @@ public static void register_users() throws Exception { @Override public void sessionEvent(SessionEvent event) { logger.atInfo().log("Got event of type: %s", event.getType()); - logger.atInfo().log("Session is:", event.getSession().toString()); + logger.atInfo().log("Session is: %s", event.getSession().toString()); } }); @@ -285,6 +288,45 @@ protected static void addNewUser(String username) throws SQLException { }, "cwms_20"); } + protected static void addVerticalDatumOffsetForExistingLocation(String location, String officeId, VerticalDatum from, VerticalDatum to, double offset, boolean isEstimate) throws SQLException { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + String desc = isEstimate ? "ESTIMATE" : ""; + final String insertSql = + "INSERT INTO AT_VERT_DATUM_OFFSET " + + " (LOCATION_CODE, VERTICAL_DATUM_ID_1, VERTICAL_DATUM_ID_2, EFFECTIVE_DATE, OFFSET, DESCRIPTION) " + + " VALUES (?, ?, ?, ?, ?, ?)"; + + db.connection(c -> { + String sql = "SELECT LOCATION_CODE FROM AV_LOC2 WHERE DB_OFFICE_ID = ? AND LOCATION_ID = ?"; + Long locationCode = null; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, officeId); + ps.setString(2, location); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + locationCode = rs.getLong(1); + } + } + } catch (SQLException ex) { + throw new RuntimeException("Unable to verify location exists for offset insert", ex); + } + if (locationCode == null) { + throw new IllegalArgumentException("Location not found for office=" + officeId + ", id=" + location); + } + try (PreparedStatement ps = c.prepareStatement(insertSql)) { + ps.setLong(1, locationCode); // LOCATION_CODE + ps.setString(2, from.toString()); // VERTICAL_DATUM_ID_1 + ps.setString(3, to.toString()); // VERTICAL_DATUM_ID_2 + ps.setDate(4, Date.valueOf("1000-01-01")); //EFFECTIVE_DATE + ps.setDouble(5, offset); // OFFSET + ps.setString(6, desc); // DESCRIPTION ("" if not estimate) + ps.executeUpdate(); + } catch (SQLException ex) { + throw new RuntimeException("Unable to insert vertical datum offset", ex); + } + }, "cwms_20"); + } + /** * Creates location with all minimum required data. * Additional calls to this function with the same location name are noop. @@ -330,6 +372,66 @@ protected static void createLocation(String location, boolean active, String off }, "cwms_20"); } + protected static void createLocationWithVerticalDatum(String location, boolean active, String office, VerticalDatum verticalDatum) throws SQLException + { + createLocation(location, active, office); + updateLocation(location, active, office, verticalDatum); + } + + private static void updateLocation(String location, boolean active, String officeId, VerticalDatum verticalDatum) throws SQLException { + + String P_LOCATION_ID = location; + String P_LOCATION_TYPE = "SITE"; + Number P_ELEVATION = 11; + String P_ELEV_UNIT_ID = "m"; + + // Pretty sure this isn't supposed to have a dash. The create doesn't check. The default create just passes null. + // If it has a dash then the offsets don't work. + // select VERTICAL_DATUM, count(*) as COUNT + // from AT_PHYSICAL_LOCATION + // group by VERTICAL_DATUM + // order by COUNT desc + // has no entries with a dash in the name (unless we've run this test with a dash). + String P_VERTICAL_DATUM = verticalDatum.toString(); + Number P_LATITUDE = 38.5757; // pretty sure that if these are 0,0 then its not inside the navd88 bounds and the offsets come back [] + Number P_LONGITUDE = -121.4789; + String P_HORIZONTAL_DATUM = "WGS84"; + String P_PUBLIC_NAME = "Integration Test Sac Dam"; + String P_LONG_NAME= null; + String P_DESCRIPTION = "for testing"; + String P_TIME_ZONE_ID = "UTC"; + String P_COUNTY_NAME = "Sacramento"; + String P_STATE_INITIAL = "CA"; + String P_ACTIVE = active ? "T" : "F"; + String P_DB_OFFICE_ID = officeId; + + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + DSLContext dslContext = getDslContext(c, officeId); + + // CWMS_LOC_PACKAGE.call_DELETE_LOCATION(dslContext.configuration(), P_LOCATION_ID, String.valueOf(DeleteRule.DELETE_LOC_CASCADE), P_DB_OFFICE_ID); + // CWMS_LOC_PACKAGE.call_CREATE_LOCATION(dslContext.configuration(), + // P_LOCATION_ID, P_LOCATION_TYPE, P_ELEVATION, P_ELEV_UNIT_ID, P_VERTICAL_DATUM, P_LATITUDE, P_LONGITUDE, + // P_HORIZONTAL_DATUM, P_PUBLIC_NAME, P_LONG_NAME, P_DESCRIPTION, P_TIME_ZONE_ID, P_COUNTY_NAME, P_STATE_INITIAL, + // P_ACTIVE, P_DB_OFFICE_ID); + + String P_IGNORENULLS = "F"; + CWMS_LOC_PACKAGE.call_UPDATE_LOCATION(dslContext.configuration(), + P_LOCATION_ID, P_LOCATION_TYPE, P_ELEVATION, P_ELEV_UNIT_ID, P_VERTICAL_DATUM, P_LATITUDE, P_LONGITUDE, + P_HORIZONTAL_DATUM, P_PUBLIC_NAME, P_LONG_NAME, P_DESCRIPTION, P_TIME_ZONE_ID, P_COUNTY_NAME, P_STATE_INITIAL, + P_ACTIVE, P_IGNORENULLS, P_DB_OFFICE_ID ); + + }); + + } + + private static DSLContext getDslContext(Connection database, String officeId) + { + DSLContext dsl = DSL.using(database, SQLDialect.ORACLE18C); + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(dsl.configuration(), officeId); + return dsl; + } + /** * Creates a location saving the data for later deletion. With the following defaults: * @@ -550,7 +652,7 @@ protected void registerCategory(LocationCategory category) { @AfterEach public void cleanupLocationGroups() throws Exception { if (this.groupsCreated.isEmpty()) { - logger.atInfo().log("No groups to cleanup."); + logger.atFine().log("No groups to cleanup."); return; } logger.atInfo().log("Cleaning up groups that tests did not remove."); @@ -575,7 +677,7 @@ public void cleanupLocationGroups() throws Exception { @AfterEach public void cleanupLocationCategories() throws Exception { if (this.categoriesCreated.isEmpty()) { - logger.atInfo().log("No location categories to cleanup."); + logger.atFine().log("No location categories to cleanup."); return; } logger.atInfo().log("Cleaning up location categories that tests did not remove."); @@ -605,7 +707,7 @@ public void cleanupLocationCategories() throws Exception { */ public static void cleanupBasins() throws Exception { if (basinsCreated.isEmpty()) { - logger.atInfo().log("No basins to cleanup."); + logger.atFine().log("No basins to cleanup."); return; } logger.atInfo().log("Cleaning up basins test did not remove."); diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ForecastInstanceControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/ForecastInstanceControllerTestIT.java index 52f00ca686..baecb267fa 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ForecastInstanceControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ForecastInstanceControllerTestIT.java @@ -389,7 +389,8 @@ void test_large_file_download_null_designator(String format) throws IOException, .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .header("Transfer-Encoding", equalTo("chunked")) + .header("Accept-Ranges", equalTo("bytes")) + .header("Content-Length", not(isEmptyOrNullString())) .contentType(equalTo("text/plain")) .extract() .response() @@ -507,7 +508,8 @@ void test_large_file_download(String format) throws IOException, URISyntaxExcept .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .header("Transfer-Encoding", equalTo("chunked")) + .header("Accept-Ranges", equalTo("bytes")) + .header("Content-Length", not(isEmptyOrNullString())) .contentType(equalTo("text/plain")) .extract() .response() @@ -773,7 +775,6 @@ void test_create_get_delete_get_lrts(String format) throws Exception { ; } - @Disabled("Update currently fails with an error trying to store a null spec id") @Test void test_create_get_update_get() throws IOException { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ForecastSpecControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/ForecastSpecControllerTestIT.java index 6d332ec2b0..3f05a89864 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ForecastSpecControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ForecastSpecControllerTestIT.java @@ -27,6 +27,7 @@ import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import static cwms.cda.api.Controllers.DESIGNATOR; import static cwms.cda.api.Controllers.ID_MASK; import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; import static io.restassured.RestAssured.given; @@ -117,7 +118,7 @@ void test_get_create_get(String format) throws IOException { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -160,7 +161,7 @@ void test_get_create_get(String format) throws IOException { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -324,7 +325,7 @@ void test_create_get_delete_get(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -345,7 +346,7 @@ void test_create_get_delete_get(String format) throws Exception { .header(AUTH_HEADER, user.toHeaderValue()) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, SPEC_ID) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .queryParam(Controllers.METHOD, JooqDao.DeleteMethod.DELETE_ALL) .when() .redirects().follow(true) @@ -362,7 +363,7 @@ void test_create_get_delete_get(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -374,6 +375,210 @@ void test_create_get_delete_get(String format) throws Exception { ; } + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT}) + void create_getAll_delete_getAll(String format) throws Exception { + + // Structure of test: + // 1) Create two specs + // 2) Call getAll and verify a list/array is returned containing both + // 3) Delete both specs + // 4) Call getAll again and verify they are not returned + + // Step 1) Create two specs + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/spk/forecast_spec_create.json"); + assertNotNull(resource); + String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(tsData); + + String specId = SPEC_ID + "TEST"; + tsData = tsData.replace("\"spec-id\": \"" + SPEC_ID + "\"", "\"spec-id\": \"" + specId + "\""); + // First spec uses SPEC_ID as-is. Second spec will replace spec-id with SPEC_ID + "-2" + String specId2 = specId + "-2"; + String tsData2 = tsData.replace("\"spec-id\": \"" + specId + "\"", "\"spec-id\": \"" + specId2 + "\""); + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create first spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSONV2) + .body(tsData) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create second spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSONV2) + .body(tsData2) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Step 2) getAll should return a list containing both specs when filtered by office and designator + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(Controllers.DESIGNATOR_MASK, "*") + .queryParam(Controllers.ID_MASK, specId + "*") + .queryParam(Controllers.SOURCE_ENTITY, ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + // verify it is an array with 2 elements and contains both spec-ids + .body("size()", equalTo(2)) + .body("[0].designator", equalTo(designator)) + .body("[1].designator", equalTo(designator)) + ; + + // Step 3) Delete both specs + truncateFcstTimeSeries(); + + // Step 4) Verify getAll no longer returns the deleted specs + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(DESIGNATOR, "*") + .queryParam(ID_MASK, specId + "*") + .queryParam(Controllers.SOURCE_ENTITY, ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + // Expect empty array + .body("size()", equalTo(0)) + ; + } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT}) + void create_getAll_with_entity_like_delete_getAll(String format) throws Exception { + + // Structure of test: + // 1) Create two specs + // 2) Call getAll and verify a list/array is returned containing both + // 3) Delete both specs + // 4) Call getAll again and verify they are not returned + + // Step 1) Create two specs + InputStream resource = this.getClass().getResourceAsStream("/cwms/cda/api/spk/forecast_spec_create.json"); + assertNotNull(resource); + String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); + assertNotNull(tsData); + + String specId = SPEC_ID + "TEST"; + tsData = tsData.replace("\"spec-id\": \"" + SPEC_ID + "\"", "\"spec-id\": \"" + specId + "\""); + // First spec uses SPEC_ID as-is. Second spec will replace spec-id with SPEC_ID + "-2" + String specId2 = specId + "-2"; + String tsData2 = tsData.replace("\"spec-id\": \"" + specId + "\"", "\"spec-id\": \"" + specId2 + "\""); + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create first spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSONV2) + .body(tsData) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create second spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSONV2) + .body(tsData2) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Step 2) getAll should return a list containing both specs when filtered by office and designator + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(Controllers.DESIGNATOR_MASK, "*") + .queryParam(Controllers.ID_MASK, specId + "*") + .queryParam(Controllers.SOURCE_ENTITY_LIKE, "%") + .when() + .redirects().follow(true) + .redirects().max(3) + .get(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + // verify it is an array with 2 elements and contains both spec-ids + .body("size()", equalTo(2)) + .body("[0].designator", equalTo(designator)) + .body("[1].designator", equalTo(designator)) + ; + + // Step 3) Delete both specs + truncateFcstTimeSeries(); + + // Step 4) Verify getAll no longer returns the deleted specs + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .queryParam(Controllers.OFFICE, OFFICE) + .queryParam(DESIGNATOR, "*") + .queryParam(ID_MASK, specId + "*") + .queryParam(Controllers.SOURCE_ENTITY, ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get(PATH) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + // Expect empty array + .body("size()", equalTo(0)) + ; + } + @Test void test_create_get_delete_get_permissions_issue() throws Exception { @@ -408,7 +613,7 @@ void test_create_get_delete_get_permissions_issue() throws Exception { .header(AUTH_HEADER, user.toHeaderValue()) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, "SPK-Daily-UKY-Test") - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .queryParam(Controllers.METHOD, JooqDao.DeleteMethod.DELETE_ALL) .when() .redirects().follow(true) @@ -424,7 +629,7 @@ void test_create_get_delete_get_permissions_issue() throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -481,7 +686,7 @@ void test_create_get_delete_get_lrts(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .header(ApiServlet.IS_NEW_LRTS, true) .when() .redirects().follow(true) @@ -508,7 +713,7 @@ void test_create_get_delete_get_lrts(String format) throws Exception { .header(AUTH_HEADER, user.toHeaderValue()) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, specId) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .queryParam(Controllers.METHOD, JooqDao.DeleteMethod.DELETE_ALL) .when() .redirects().follow(true) @@ -525,7 +730,7 @@ void test_create_get_delete_get_lrts(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -556,7 +761,7 @@ void test_create_get_update_get(String format) throws IOException { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -598,7 +803,7 @@ void test_create_get_update_get(String format) throws IOException { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) @@ -639,7 +844,7 @@ void test_create_get_update_get(String format) throws IOException { .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) - .queryParam(Controllers.DESIGNATOR, designator) + .queryParam(DESIGNATOR, designator) .when() .redirects().follow(true) .redirects().max(3) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java index e6e8a575ca..e6410d6d66 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LevelsControllerTestIT.java @@ -1725,7 +1725,7 @@ void testStoreSeasonalLevel() throws Exception { ZonedDateTime levelDate = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:00Z"), ZoneId.of("UTC")); List values = new ArrayList<>(); int numValues = 12; - for (int i = 1; i <= numValues; i++) { + for (int i = 0; i < numValues; i++) { values.add(new SeasonalValueBean.Builder() .withValue(i + 1.0) .withOffsetMonths(i) @@ -1778,6 +1778,7 @@ void testStoreSeasonalLevel() throws Exception { .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) .body("expiration-date", equalTo(levelDate.plusYears(50).toInstant().toString())) + .body("interval-months", equalTo(12)) .body("seasonal-values.size()", is(numValues)); } @@ -1790,7 +1791,7 @@ void testRetrieveAllSeasonalLevel() throws Exception { ZonedDateTime levelDate = ZonedDateTime.ofInstant(Instant.parse("2024-01-01T00:00:00Z"), ZoneId.of("UTC")); List values = new ArrayList<>(); int numValues = 12; - for (int i = 1; i <= numValues; i++) { + for (int i = 0; i < numValues; i++) { values.add(new SeasonalValueBean.Builder() .withValue(i + 1.0) .withOffsetMonths(i) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTest.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTest.java deleted file mode 100644 index 4483ec70b7..0000000000 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package cwms.cda.api; - -import java.util.HashMap; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.codahale.metrics.MetricRegistry; - -import cwms.cda.api.enums.Nation; -import cwms.cda.data.dto.Location; -import cwms.cda.formatters.Formats; -import fixtures.TestHttpServletResponse; -import fixtures.TestServletInputStream; -import io.javalin.http.Context; -import io.javalin.http.HandlerType; -import io.javalin.http.util.ContextUtil; -import io.javalin.plugin.json.JavalinJackson; -import io.javalin.plugin.json.JsonMapperKt; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -class LocationControllerTest extends ControllerTest -{ - @Test - void testDeserializeLocationXml() - { - String xml = loadResourceAsString("cwms/cda/api/location_create.xml"); - assertNotNull(xml); - Location location = Formats.parseContent(Formats.parseHeader(Formats.XML, Location.class), - xml, Location.class); - assertNotNull(location); - assertEquals("LOC_TEST", location.getName()); - assertEquals("LRL", location.getOfficeId()); - assertEquals("NGVD-29", location.getHorizontalDatum()); - assertEquals("UTC", location.getTimezoneName()); - assertEquals(Nation.US, location.getNation()); - } - - @Test - void testDeserializeLocationJSON() - { - final String OFFICE = "SPK"; - final String json = loadResourceAsString("cwms/cda/api/location_create_spk.json"); - - assertNotNull(json); - Location location = Formats.parseContent(Formats.parseHeader(Formats.JSON, Location.class), - json, Location.class); - assertNotNull(location); - assertEquals("LOC_TEST", location.getName()); - assertEquals(OFFICE, location.getOfficeId()); - assertEquals("NGVD-29", location.getHorizontalDatum()); - assertEquals("UTC", location.getTimezoneName()); - assertEquals(Nation.US, location.getNation()); - } - - /** - * Test of getOne method, of class LocationController. - */ - @Test - void test_basic_operations() throws Exception { - final String testBody = ""; - LocationController instance = new LocationController(new MetricRegistry()); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = new TestHttpServletResponse(); - HashMap attributes = new HashMap<>(); - attributes.put(ContextUtil.maxRequestSizeKey,Integer.MAX_VALUE); - attributes.put(JsonMapperKt.JSON_MAPPER_KEY,new JavalinJackson()); - - when(request.getInputStream()).thenReturn(new TestServletInputStream(testBody)); - when(request.getQueryString()).thenReturn("office=LRL"); - - final Context context = ContextUtil.init(request,response,"*", new HashMap<>(), HandlerType.GET,attributes); - context.attribute("database",getTestConnection()); - - when(request.getAttribute("database")).thenReturn(getTestConnection()); - - assertNotNull( context.attribute("database"), "could not get the connection back as an attribute"); - assertNotNull(context.queryParam("office"), "could not get the office back as a query parameter" ); - - String locationId = "SimpleNoAlias"; - - instance.getOne(context, locationId); - assertEquals(200,context.status(), "incorrect status code returned"); - } -} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java index 15d21f7492..d97a9a1eb1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/LocationControllerTestIT.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import cwms.cda.data.dao.LocationCategoryDao; import cwms.cda.data.dao.LocationGroupDao; +import cwms.cda.data.dao.VerticalDatum; import fixtures.CwmsDataApiSetupCallback; import java.time.ZoneId; import java.util.ArrayList; @@ -37,6 +38,7 @@ import cwms.cda.data.dto.LocationCategory; import cwms.cda.data.dto.LocationGroup; import cwms.cda.formatters.ContentType; +import cwms.cda.data.dto.VerticalDatumInfo; import fixtures.TestAccounts.KeyUser; import io.restassured.filter.log.LogDetail; import org.jooq.DSLContext; @@ -58,6 +60,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isOneOf; @@ -123,6 +126,29 @@ void cleanup() // ignore } } + + try { + String officeId = "SPK"; + String json = loadResourceAsString("cwms/cda/api/location_create_spk.json"); + Location location = new Location.Builder(Formats.parseContent(Formats.parseHeader(Formats.JSON, Location.class), json, Location.class)) + .withOfficeId(officeId) + .build(); + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, "true") + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/locations/" + location.getName()) + .then() + .log().ifValidationFails(LogDetail.ALL,true); + } catch (Exception e) { + //ignore + } + } @Test @@ -227,6 +253,125 @@ void test_location_create_get_delete() throws Exception { .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + @Test + void test_get_one_with_datum_param() throws Exception { + KeyUser user = KeyUser.SPK_NORMAL; + String officeId = user.getOperatingOffice(); + String locNgvd = "LocDatumNGVD29"; + + // Create location with explicit offset + createLocationWithVerticalDatum(locNgvd, true, officeId, VerticalDatum.NGVD29); + addVerticalDatumOffsetForExistingLocation(locNgvd, officeId, VerticalDatum.NGVD29, VerticalDatum.NAVD88, -0.5, true); + + // Request NGVD29 for NGVD29 location + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, officeId) + .queryParam(DATUM, VerticalDatum.NGVD29.toString()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locNgvd) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("vertical-datum", equalTo(VerticalDatum.NGVD29.toString())) + .body("elevation.doubleValue()", closeTo(11, 1e-6)); + + // Request NAVD88 for NGVD29 location + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, officeId) + .queryParam(DATUM, VerticalDatum.NAVD88.toString()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + locNgvd) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("vertical-datum", equalTo(VerticalDatum.NAVD88.toString())) + // 11 native NGVD-29 + (-0.5) offset to NAVD-88 = 10.5 + .body("elevation.doubleValue()", closeTo(10.5, 1e-6)); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/locations/" + locNgvd) + .then() + .log().ifValidationFails(LogDetail.ALL,true); + } + + @Test + void test_get_all_with_datum_param() throws Exception { + String locNgvd = "LocDatumNGVD29All"; + KeyUser user = KeyUser.SPK_NORMAL; + String officeId = user.getOperatingOffice(); + + createLocationWithVerticalDatum(locNgvd, true, officeId, VerticalDatum.NGVD29); + addVerticalDatumOffsetForExistingLocation(locNgvd, officeId, VerticalDatum.NGVD29, VerticalDatum.NAVD88, -0.5, true); + + // Request NGVD29 for NGVD29 location + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, officeId) + .queryParam(DATUM, VerticalDatum.NGVD29.toString()) + .queryParam(NAMES, locNgvd) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].vertical-datum", equalTo(VerticalDatum.NGVD29.toString())) + .body("[0].elevation.doubleValue()", closeTo(11.0, 1e-6)); + + // Request NAVD88 for NGVD29 location + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, officeId) + .queryParam(DATUM, VerticalDatum.NAVD88.toString()) + .queryParam(NAMES, locNgvd) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].vertical-datum", equalTo(VerticalDatum.NAVD88.toString())) + // 11 native NGVD-29 + (-0.5) offset to NAVD-88 = 10.5 + .body("[0].elevation.doubleValue()", closeTo(10.5, 1e-6)); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/locations/" + locNgvd) + .then() + .log().ifValidationFails(LogDetail.ALL,true); + } + @Test void test_location_create_aliased() throws Exception { // Tests for https://github.com/USACE/cwms-data-api/issues/1080 @@ -493,6 +638,21 @@ void test_create_update() throws Exception { .statusCode(is(HttpServletResponse.SC_OK)) .body("name", equalTo(updatedLocationName)); + // delete location + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSON) + .queryParam(OFFICE, user.getOperatingOffice()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/locations/" + updatedLocationName) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("name", equalTo(updatedLocationName)); + // delete location given() .log().ifValidationFails(LogDetail.ALL,true) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java b/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java index 2d365d4c03..660b554955 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/OpenApiDocTest.java @@ -22,8 +22,10 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.ImportDeclaration; +import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.expr.ArrayInitializerExpr; import com.github.javaparser.ast.expr.ClassExpr; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.FieldAccessExpr; @@ -31,6 +33,7 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.resolution.Resolvable; import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration; +import com.github.javaparser.resolution.types.ResolvedType; import com.google.common.flogger.FluentLogger; import helpers.OpenApiDocInfo; import helpers.OpenApiDocTestInfo; @@ -39,6 +42,7 @@ import helpers.OpenApiParamUsageInfo; import helpers.OpenApiTestHelper; import io.javalin.apibuilder.CrudHandler; +import io.javalin.http.Context; import io.javalin.http.Handler; import java.io.IOException; import java.lang.reflect.Field; @@ -46,6 +50,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -53,7 +58,6 @@ import java.util.stream.Stream; import javax.servlet.http.HttpServletResponse; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; @@ -79,9 +83,9 @@ void test_crud_handler_documentation(OpenApiDocTestInfo testInfo) throws IOExcep assertAll(buildTestAssertions(compilationUnit, testInfo)); } - @Test +// @Test void test_time_series_controller() throws IOException { - OpenApiDocTestInfo testInfo = OpenApiTestHelper.readOpenApiDocs(CrudHandler.class, StateController.class); + OpenApiDocTestInfo testInfo = OpenApiTestHelper.readOpenApiDocs(Handler.class, TimeSeriesFilteredController.class); CompilationUnit compilationUnit = OpenApiTestHelper.readCompilationUnit(testInfo.getClazz()); assertAll(buildTestAssertions(compilationUnit, testInfo)); } @@ -98,7 +102,7 @@ private Executable validateOpenApiDoc(CompilationUnit unit, OpenApiDocInfo testI output = testIgnoredMethod(unit, testInfo, clazz); } else { OpenApiParamUsage parsedParamInfo = parseParamInfo(unit, clazz, testInfo.getMethod()); - output = testMethod(testInfo, parsedParamInfo); + output = testMethod(unit, testInfo, parsedParamInfo, clazz); } return output; } @@ -119,6 +123,7 @@ private Executable testIgnoredMethod(CompilationUnit unit, OpenApiDocInfo testIn .filter(exp -> exp.getNameAsString().equals("json")) .findFirst(); + String methodRef = buildMethodRef(method, clazz); try { boolean usesStatus = statusCall.isPresent(); boolean isCorrectCode = statusCall.stream() @@ -131,25 +136,33 @@ private Executable testIgnoredMethod(CompilationUnit unit, OpenApiDocInfo testIn .map("CdaError.notImplemented()"::equals) .orElse(false); return () -> assertAll( - "Testing ignored method " + method.getNameAsString() + ": Incorrect response for ignored endpoint. Expecting `ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented())`", + "Testing ignored method " + method.getNameAsString() + " " + methodRef + ": Incorrect response for ignored endpoint. Expecting `ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented())`", () -> assertTrue(usesStatus && isCorrectCode, "Incorrect status code used, context should provide HttpServletResponse.SC_NOT_IMPLEMENTED."), () -> assertTrue(usesJson && isCorrectJson, "Incorrect JSON returned, context should respond with CdaError.notImplemented()")); } catch (Exception ex) { - return () -> fail("Testing ignored method " + method.getNameAsString() + ": Error analyzing method. Expected `ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented());`.", ex); + return () -> fail("Testing ignored method " + method.getNameAsString() + " " + methodRef + ": Error analyzing method. Expected `ctx.status(HttpServletResponse.SC_NOT_IMPLEMENTED).json(CdaError.notImplemented());`.", ex); } } - private Executable testMethod(OpenApiDocInfo testInfo, - OpenApiParamUsage parsedParamInfo) { + private String buildMethodRef(MethodDeclaration method, Class clazz) { + //Creates a link in IntelliJ logs so we can easily jump to the file and declaration. + return "(" + clazz.getSimpleName() + ".java:" + method.getName().getBegin().map(p -> p.line).orElse(-1) + ")"; + } + + private Executable testMethod(CompilationUnit unit, OpenApiDocInfo testInfo, + OpenApiParamUsage parsedParamInfo, Class clazz) { List expectedQueryParameters = testInfo.getQueryParameters(); List expectedPathParameters = testInfo.getPathParameters(); Set receivedQueryParameters = parsedParamInfo.getQueryParams(); Set receivedPathParameters = parsedParamInfo.getPathParams(); OpenApiParamUsageInfo receivedResourceId = parsedParamInfo.getResourceId(); - return () -> assertAll("Testing " + testInfo.getMethod().getName(), + MethodDeclaration method = getMethodDeclaration(unit, testInfo.getMethod()); + String methodRef = buildMethodRef(method, clazz); + + return () -> assertAll("Testing " + testInfo.getMethod().getName() + " " + methodRef, () -> testQueryParameters(expectedQueryParameters, receivedQueryParameters), () -> testPathParameters(expectedPathParameters, receivedPathParameters, receivedResourceId)); } @@ -194,9 +207,11 @@ private void testPathParameters(List expectedPathParameters, S String missingInfo = missingItems.stream() .map(OpenApiParamInfo::getName) .collect(Collectors.joining(", ")); - assertAll(() -> assertTrue(receivedItems.isEmpty(), "Found used undocumented path parameter: " + extraInfo), + assertAll( + () -> assertTrue(receivedItems.isEmpty(), "Found used undocumented path parameter: " + extraInfo), () -> assertTrue(missingItems.isEmpty(), "Found documented path parameter that is not used: " + missingInfo), - () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages)))); + () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages, true))) + ); } private void testQueryParameters(List expectedQueryParameters, @@ -231,11 +246,11 @@ private void testQueryParameters(List expectedQueryParameters, .collect(Collectors.joining(", ")); assertAll(() -> assertTrue(receivedItems.isEmpty(), "Found used undocumented query parameter: " + extraInfo), () -> assertTrue(missingItems.isEmpty(), "Found documented query parameter that is not used: " + missingInfo), - () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages)))); + () -> assertAll(expectedParams.stream().map(expectedParam -> testParamInfo(expectedParam, verifiedUsages, false)))); } private Executable testParamInfo(OpenApiParamInfo expectedParam, - Set receivedQueryParameters) { + Set receivedQueryParameters, boolean pathParam) { OpenApiParamUsageInfo receivedInfo = receivedQueryParameters.stream() .filter(receivedUsageInfo -> receivedUsageInfo.getParamInfo() .getName() @@ -248,7 +263,18 @@ private Executable testParamInfo(OpenApiParamInfo expectedParam, //Real tests return () -> assertAll(() -> assertTrue(receivedInfo.isUsed(), "Unable to find a usage of documented parameter: " + expectedParam.getName()), - () -> assertTrue(receivedInfo.isNullHandled(), "Unable to find a null handled usage of documented parameter: " + expectedParam.getName())); + () -> assertTrue(receivedInfo.isNullHandled(), "Unable to find a null handled usage of documented parameter: " + expectedParam.getName()), + // Disabled type checking due to many parameters being read as strings and then converted, + // which is a valid way to read parameters, but makes it difficult to verify the type is correct. + // We can re-enable this in the future if we want to be more strict about how parameters are read. + //() -> assertEquals(receivedInfo.getParamInfo().getType(), expectedParam.getType(), "Incorrect type for parameter: " + expectedParam.getName()), + () -> assertEquals(receivedInfo.getParamInfo().getName(), expectedParam.getName(), "Incorrect name for parameter: " + expectedParam.getName()), + () -> { + if (!pathParam && !expectedParam.ignoreRequired()) // Path parameters are always required, so we don't need to check that. + { + assertEquals(receivedInfo.getParamInfo().isRequired(), expectedParam.isRequired(), "Incorrect required status for parameter: " + expectedParam.getName()); + } + }); } private OpenApiParamUsage parseParamInfo(CompilationUnit unit, Class clazz, Method method) { @@ -256,8 +282,10 @@ private OpenApiParamUsage parseParamInfo(CompilationUnit unit, Class clazz, M String context = methodDeclaration.getParameter(0).getNameAsString(); List methodCalls = methodDeclaration.findAll(MethodCallExpr.class); - Set optionalTypedQueryParams = readParamUsagesFromCall(methodCalls, call -> readQueryParamAsClassFromCall(unit, context, clazz, call), "queryParamAsClass"); + Set optionalTypedQueryParams = readParamUsagesSetFromCall(methodCalls, call -> readQueryParamAsClassFromCall(unit, context, clazz, call), "queryParamAsClass"); Set optionalDoubleQueryParams = readParamUsagesFromCall(methodCalls, call -> readUsageFromCall(unit, clazz, call, false), "queryParamAsDouble"); + Set filteredTsParam = readParamUsagesFromCall(methodCalls, this::findTsParamsFromUsage, "from"); + Set ignoredPathParams = readParamUsagesFromCall(methodCalls, this::readIgnoredPathParameter, "logUnusedPathParameter"); Set optionalStringQueryParams = methodCalls.stream() .filter(call -> call.getNameAsString().equals("queryParam")) @@ -290,12 +318,14 @@ private OpenApiParamUsage parseParamInfo(CompilationUnit unit, Class clazz, M queryParams.addAll(optionalTimeQueryParams); queryParams.addAll(requiredTimeQueryParams); queryParams.addAll(optionalDoubleQueryParams); + queryParams.addAll(filteredTsParam); Set pathParams = methodCalls.stream() .filter(call -> call.getNameAsString().equals("pathParam")) .map(call -> readUsageFromCall(unit, clazz, call, true)) .collect(Collectors.toSet()); + pathParams.addAll(ignoredPathParams); OpenApiParamUsageInfo resourceId = null; @@ -312,11 +342,31 @@ private OpenApiParamUsage parseParamInfo(CompilationUnit unit, Class clazz, M return new OpenApiParamUsage(pathParams, queryParams, resourceId); } - private OpenApiParamUsageInfo readQueryParamAsClassFromCall(CompilationUnit unit, String context, Class clazz, MethodCallExpr call) { + private OpenApiParamUsageInfo findTsParamsFromUsage(MethodCallExpr call) { + boolean isRightFunc = call.getScope() + .filter(Expression::isFieldAccessExpr) + .map(Expression::asFieldAccessExpr) + .map(s -> s.toString().equals("FilteredTimeSeriesParameters.Builder")) + .orElse(false); + OpenApiParamUsageInfo output = null; + if (isRightFunc) { + Expression arg0 = call.getArgument(0); + ResolvedType type = arg0.calculateResolvedType(); + if (type.isReferenceType()) { + String qualifiedName = type.asReferenceType().getQualifiedName(); + if (qualifiedName.equalsIgnoreCase(Context.class.getName())) { + output = new OpenApiParamUsageInfo(new OpenApiParamInfo(Controllers.QUERY, false, String.class), true, true); + } + } + } + return output; + } + + private Set readQueryParamAsClassFromCall(CompilationUnit unit, String context, Class clazz, MethodCallExpr call) { return call.getScope() .map(scope -> { if (scope.isNameExpr()) { - return readQueryParamAsClassFromContextCall(unit, clazz, call); + return Set.of(readQueryParamAsClassFromContextCall(unit, clazz, call)); } else { return readQueryParamAsClassFromControllersCall(unit, clazz, call); } @@ -338,24 +388,43 @@ private OpenApiParamUsageInfo readQueryParamAsClassFromContextCall(CompilationUn return new OpenApiParamUsageInfo(new OpenApiParamInfo(paramName, false, paramClass), used, nullHandled); } - private OpenApiParamUsageInfo readQueryParamAsClassFromControllersCall(CompilationUnit unit, Class clazz, MethodCallExpr call) { + private Set readQueryParamAsClassFromControllersCall(CompilationUnit unit, Class clazz, MethodCallExpr call) { Expression arg1 = call.getArgument(1); - Class type; - String name; + Set output = new HashSet<>(); if (arg1.isArrayCreationExpr()) { //Context, String[], Class, T, {metrics}, {className} - type = identifyClassFromExpression(unit, clazz, call.getArgument(2).asClassExpr()); - name = parseParameterName(arg1.asArrayCreationExpr().getInitializer().orElse(null).getValues().get(0)); + NodeList values = arg1.asArrayCreationExpr() + .getInitializer() + .map(ArrayInitializerExpr::getValues) + .orElse(new NodeList<>()); + Class type = identifyClassFromExpression(unit, clazz, call.getArgument(2).asClassExpr()); + values.forEach(value -> { + String name = parseParameterName(value); + output.add(new OpenApiParamUsageInfo(new OpenApiParamInfo(name, false, type), true, true)); + }); } else if (arg1.isClassExpr()) { //Context, Class, T, Name, [Aliases] - type = identifyClassFromExpression(unit, clazz, arg1.asClassExpr()); - name = parseParameterName(call.getArgument(3)); + Class type = identifyClassFromExpression(unit, clazz, arg1.asClassExpr()); + String name = parseParameterName(call.getArgument(3)); + output.add(new OpenApiParamUsageInfo(new OpenApiParamInfo(name, false, type), true, true)); } else { //Unknown case for queryParamAsClass (new method to handle? throw new UnsupportedOperationException("Unsupported argument[1] type for queryParamAsClass: " + arg1.getClass()); } - return new OpenApiParamUsageInfo(new OpenApiParamInfo(name, false, type), true, true); + return output; + } + + private Set readParamUsagesSetFromCall(List methodCalls, + Function> paramReader, + String... functions) { + List realFunctions = Arrays.asList(functions); + return methodCalls.stream() + .filter(call -> realFunctions.contains(call.getNameAsString())) + .map(paramReader) + .filter(s -> !s.isEmpty()) + .flatMap(Set::stream) + .collect(Collectors.toSet()); } private Set readParamUsagesFromCall(List methodCalls, @@ -365,6 +434,7 @@ private Set readParamUsagesFromCall(List return methodCalls.stream() .filter(call -> realFunctions.contains(call.getNameAsString())) .map(paramReader) + .filter(Objects::nonNull) .collect(Collectors.toSet()); } @@ -378,6 +448,12 @@ private Set readJavaTimeFromCall(MethodCallExpr call, boo new OpenApiParamUsageInfo(new OpenApiParamInfo(Controllers.TIMEZONE, required, type), used, nullHandled)); } + private OpenApiParamUsageInfo readIgnoredPathParameter(MethodCallExpr call) { + //Should never have scope, formatted as logUnusedPathParameter(Context ctx, String pathParam, String reason) + String param = parseParameterName(call.getArgument(1)); + return new OpenApiParamUsageInfo(new OpenApiParamInfo(param, true, String.class), true, true); + } + private OpenApiParamUsageInfo readUsageFromCall(CompilationUnit unit, Class clazz, MethodCallExpr call, boolean required) { //We have a scope, so it's called from something like context. return call.getScope().map(exp -> { @@ -393,7 +469,7 @@ private OpenApiParamUsageInfo readUsageFromCall(CompilationUnit unit, Class c boolean used = true; boolean nullHandled = true; if (!required) { - //Check if null is handled via getOrDefault + //TODO: Check if null is handled via getOrDefault } return new OpenApiParamUsageInfo(new OpenApiParamInfo(paramName, required, paramClass), used, nullHandled); }).orElseGet(() -> { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/ParametersControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/ParametersControllerTestIT.java index 76ecfdcfdc..eeb61370ab 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/ParametersControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/ParametersControllerTestIT.java @@ -12,6 +12,8 @@ import static cwms.cda.api.Controllers.OFFICE; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.greaterThan; +import io.restassured.response.ValidatableResponse; @Tag("integration") class ParametersControllerTestIT extends DataApiTestIT @@ -37,23 +39,29 @@ void test_get_all_parameters(GetAllTest test) .contentType(is(test._expectedContentType)); } - @ParameterizedTest - @EnumSource(GetAllLegacyTest.class) - void test_get_all_parameters_legacy_types(GetAllLegacyTest test) - { - given() - .log().ifValidationFails(LogDetail.ALL,true) - .queryParam(FORMAT, test._accept) - .when() - .redirects().follow(true) - .redirects().max(3) - .get("/parameters/") - .then() - .log().ifValidationFails(LogDetail.ALL,true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_OK)) - .contentType(is(test._expectedContentType)); - } + @ParameterizedTest + @EnumSource(GetAllLegacyTest.class) + void test_get_all_parameters_legacy_types(GetAllLegacyTest test) + { + ValidatableResponse response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .queryParam(FORMAT, test._accept) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/parameters/") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(test._expectedContentType)); + + // ensure default-english-unit and default-si-unit values are no longer the same + if (test == GetAllLegacyTest.JSON) { + response.body("parameters.parameters.findAll { it.'default-english-unit' != it.'default-si-unit' }.size()", + greaterThan(0)); + } + } enum GetAllLegacyTest { diff --git a/cwms-data-api/src/test/java/cwms/cda/api/RangeParserTest.java b/cwms-data-api/src/test/java/cwms/cda/api/RangeParserTest.java new file mode 100644 index 0000000000..05452447a0 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/RangeParserTest.java @@ -0,0 +1,129 @@ +package cwms.cda.api; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RangeParserTest { + + @Test + void testResume() { + List ranges = RangeParser.parse("bytes=100-"); + assertNotNull(ranges); + assertEquals(1, ranges.size()); + assertArrayEquals(new long[]{100L, -1L}, ranges.get(0)); + } + + @Test + void testFirstK() { + List ranges = RangeParser.parse("bytes=0-1000"); + assertNotNull(ranges); + assertEquals(1, ranges.size()); + assertArrayEquals(new long[]{0L, 1000L}, ranges.get(0)); + } + + @Test + void testFirstOpen() { + List ranges = RangeParser.parse("bytes=0-"); + assertNotNull(ranges); + assertEquals(1, ranges.size()); + assertArrayEquals(new long[]{0L, -1L}, ranges.get(0)); + } + + @Test + void testSuffixOpen() { + List ranges = RangeParser.parse("bytes=-50"); + assertNotNull(ranges); + assertEquals(1, ranges.size()); + assertArrayEquals(new long[]{-1L, 50L}, ranges.get(0)); + } + + + @Test + void testTwoPart() { + List ranges = RangeParser.parse("bytes=0-10,99-100"); + assertNotNull(ranges); + assertEquals(2, ranges.size()); + assertArrayEquals(new long[]{0L, 10L}, ranges.get(0)); + assertArrayEquals(new long[]{99L, 100L}, ranges.get(1)); + } + + + @Test + void testMultiParse() { + List ranges = RangeParser.parse("bytes=0-99,200-299,-50"); + assertNotNull(ranges); + assertEquals(3, ranges.size()); + assertArrayEquals(new long[]{0L, 99L}, ranges.get(0)); + assertArrayEquals(new long[]{200L, 299L}, ranges.get(1)); + assertArrayEquals(new long[]{-1L, 50L}, ranges.get(2)); + } + + + @Test + void testTwoWeird() { + List ranges = RangeParser.parse("bytes=0-0,-1"); + assertNotNull(ranges); + assertEquals(2, ranges.size()); + assertArrayEquals(new long[]{0L, 0L}, ranges.get(0)); + assertArrayEquals(new long[]{-1L, 1L}, ranges.get(1)); + } + + @Test + void testNotBytes() { + assertThrows(IllegalArgumentException.class, () -> RangeParser.parse("bits=0-10")); + } + + + @Test + void testSuffixDoubleNeg() { + assertThrows(IllegalArgumentException.class, () -> RangeParser.parse("bytes=--64")); + } + + + @Test + void testSuffixClosed() { + assertThrows(IllegalArgumentException.class, () -> + RangeParser.parse("bytes=-50-100")); + } + + + @Test + void testSuffixDoubleClosed() { + assertThrows(IllegalArgumentException.class, () -> RangeParser.parse("bytes=-50--100")); + } + + @Test + void testInterpret(){ + + assertArrayEquals(new long[]{0L, 10L}, RangeParser.interpret(new long[]{0L, 10L}, 100)); + assertArrayEquals(new long[]{0L, 0L}, RangeParser.interpret(new long[]{0L, 0L}, 100)); + assertArrayEquals(new long[]{8L, 12L}, RangeParser.interpret(new long[]{8L, 12L}, 100)); + assertArrayEquals(new long[]{8L, 99L}, RangeParser.interpret(new long[]{8L, 100L}, 100)); + assertArrayEquals(new long[]{8L, 99L}, RangeParser.interpret(new long[]{8L, 200L}, 100)); + + // typical resume bytes=10- + assertArrayEquals(new long[]{10L, 99L}, RangeParser.interpret(new long[]{10L, -1L}, 100)); + + // bytes=0-0 + assertArrayEquals(new long[]{0L, 0L}, RangeParser.interpret(new long[]{0L, 0L}, 100)); + // bytes=-1 + assertArrayEquals(new long[]{99L, 99L}, RangeParser.interpret(new long[]{-1L, 1L}, 100)); + + // bytes=-50 + assertArrayEquals(new long[]{50L, 99L}, RangeParser.interpret(new long[]{-1L, 50L}, 100)); + + } + + @Test + void testInvalidInterp() { + // They requested 100-200 but our file is 100 long (only 0-99). + assertThrows(IllegalArgumentException.class, () -> RangeParser.interpret(new long[]{100L, 200L}, 100)); + assertThrows(IllegalArgumentException.class, () -> RangeParser.interpret(new long[]{200L, 100L}, 100)); + assertThrows(IllegalArgumentException.class, () -> RangeParser.interpret(new long[]{100L, 100L}, 100)); + + } + +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TextTimeSeriesControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TextTimeSeriesControllerTestIT.java index 358190eacb..ccce8306a2 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TextTimeSeriesControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TextTimeSeriesControllerTestIT.java @@ -24,12 +24,21 @@ package cwms.cda.api; -import static cwms.cda.api.Controllers.*; +import static cwms.cda.api.Controllers.CLOB_ID; +import static cwms.cda.api.Controllers.VERSION_DATE; import static cwms.cda.data.dao.DaoTest.getDslContext; import static io.restassured.RestAssured.given; import static java.util.stream.Collectors.toMap; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isEmptyOrNullString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.flogger.FluentLogger; @@ -38,20 +47,16 @@ import cwms.cda.data.dto.texttimeseries.TextTimeSeries; import cwms.cda.formatters.Formats; import cwms.cda.formatters.json.JsonV2; -import cwms.cda.helpers.DateUtils; import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; import io.restassured.response.ResponseBody; -import io.restassured.response.ValidatableResponse; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.time.ZonedDateTime; import java.util.Map; import java.util.Objects; import java.util.Random; @@ -191,6 +196,7 @@ void test_create_regular(String format) throws Exception { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(1)) .body("regular-text-values[0].text-value", equalTo("newly created text value")) .statusCode(is(HttpServletResponse.SC_OK)); @@ -209,7 +215,6 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti // with the old LRTS identifier and verify it fails // make sure it doesn't exist - // this will return 200 but the lists should be empty. String startStr = "2005-02-01T08:00:00Z"; String endStr = "2005-02-01T14:00:00Z"; String tsIdentifier = "TsTextTestLoc.Flow.Inst.1DayLocal.0.lrts-test"; @@ -262,6 +267,7 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, tsIdentifier) .queryParam(Controllers.BEGIN,startStr) @@ -275,6 +281,7 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(1)) .body("regular-text-values[0].text-value", equalTo("newly created text value")) .statusCode(is(HttpServletResponse.SC_OK)); @@ -283,6 +290,7 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .header(AUTHORIZATION, user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, tsIdentifier) .queryParam(Controllers.TEXT_MASK, "*") @@ -300,6 +308,7 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(Controllers.OFFICE, OFFICE) .queryParam(Controllers.NAME, tsIdentifier) .queryParam(Controllers.BEGIN,startStr) @@ -313,8 +322,10 @@ void test_create_local_regular_new_LRTS_identifier(String format) throws Excepti .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(0)) .statusCode(is(HttpServletResponse.SC_OK)); + assertThat = given() .log().ifValidationFails(LogDetail.ALL,true) @@ -358,6 +369,7 @@ void test_retrieve_regular() { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(5)) .statusCode(is(HttpServletResponse.SC_OK)); @@ -389,6 +401,7 @@ void test_update_regular(String format) throws Exception { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(5)) .statusCode(is(HttpServletResponse.SC_OK)); @@ -416,7 +429,7 @@ void test_update_regular(String format) throws Exception { .statusCode(is(HttpServletResponse.SC_OK)); //3)retrieve and verify - ValidatableResponse response = given() + given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) .queryParam(Controllers.OFFICE, OFFICE) @@ -432,6 +445,7 @@ void test_update_regular(String format) throws Exception { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(5)) .body("regular-text-values[0].text-value", equalTo("still great")) .statusCode(is(HttpServletResponse.SC_OK)); @@ -452,8 +466,6 @@ void test_delete_regular(String format) { String startStr = "2005-01-01T03:00:00Z"; String endStr = "2005-01-01T04:00:00Z"; - ZonedDateTime startZdt = DateUtils.parseUserDate(startStr, "UTC"); - ZonedDateTime endZdt = DateUtils.parseUserDate(endStr, "UTC"); // 1)retrieve and verify given() @@ -472,6 +484,7 @@ void test_delete_regular(String format) { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(2)) // assert the first regular-text-value item is as expected .body("regular-text-values[0].text-value", equalTo(EXPECTED_TEXT_VALUE)) @@ -515,6 +528,7 @@ void test_delete_regular(String format) { .assertThat() .body("standard-text-catalog", nullValue()) .body("standard-text-values", nullValue()) + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(0)) .statusCode(is(HttpServletResponse.SC_OK)); } @@ -582,6 +596,7 @@ void test_large_data_url(String format) throws Exception { .then() .log().ifValidationFails(LogDetail.ALL, true) .assertThat() + .body("regular-text-values", notNullValue()) .body("regular-text-values.size()", equalTo(1)) .statusCode(is(HttpServletResponse.SC_OK)) .body("regular-text-values[0].text-value", is(nullValue())) @@ -617,7 +632,8 @@ void test_large_data_url(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .header("Transfer-Encoding", equalTo("chunked")) + .header("Accept-Ranges", equalTo("bytes")) + .header("Content-Length", not(isEmptyOrNullString())) .contentType(equalTo("text/plain")) .extract() .response() diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesCategoryControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesCategoryControllerTestIT.java index b5e28b6cd4..e6f9a9149c 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesCategoryControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesCategoryControllerTestIT.java @@ -24,33 +24,203 @@ package cwms.cda.api; -import cwms.cda.ApiServlet; -import fixtures.FunctionalSchemas; -import fixtures.TestAccounts; -import io.restassured.filter.log.LogDetail; - -import org.junit.jupiter.api.Tag; +import static cwms.cda.api.Controllers.CASCADE_DELETE; +import static cwms.cda.api.Controllers.CWMS_OFFICE; +import static cwms.cda.api.Controllers.IGNORE_NULLS; +import static cwms.cda.api.Controllers.OFFICE; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import com.google.common.flogger.FluentLogger; +import cwms.cda.ApiServlet; +import cwms.cda.api.errors.NotFoundException; +import cwms.cda.data.dao.TimeSeriesCategoryDao; +import cwms.cda.data.dao.TimeSeriesGroupDao; import cwms.cda.data.dto.TimeSeriesCategory; +import cwms.cda.data.dto.TimeSeriesGroup; import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; - +import fixtures.CwmsDataApiSetupCallback; +import fixtures.FunctionalSchemas; +import fixtures.TestAccounts; +import io.restassured.filter.log.LogDetail; +import java.util.ArrayList; +import java.util.List; import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.jooq.Configuration; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static cwms.cda.api.Controllers.CASCADE_DELETE; -import static cwms.cda.api.Controllers.OFFICE; -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; - @Tag("integration") class TimeSeriesCategoryControllerTestIT extends DataApiTestIT { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private final List categoriesToCleanup = new ArrayList<>(); + private final List groupsToCleanup = new ArrayList<>(); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; TestAccounts.KeyUser user2 = TestAccounts.KeyUser.SWT_NORMAL; + @AfterEach + void clear_data() throws Exception { + CwmsDatabaseContainer db = CwmsDataApiSetupCallback.getDatabaseLink(); + db.connection(c -> { + Configuration configuration = DSL.using(c).configuration(); + TimeSeriesGroupDao groupDao = new TimeSeriesGroupDao(configuration.dsl()); + TimeSeriesCategoryDao categoryDao = new TimeSeriesCategoryDao(configuration.dsl()); + + for (TimeSeriesGroup group : groupsToCleanup) { + try { + groupDao.unassignAllTs(group, group.getOfficeId()); + if (!group.getOfficeId().equalsIgnoreCase(CWMS_OFFICE)) { + groupDao.delete(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), false); + } + } catch (NotFoundException e) { + logger.atConfig().withCause(e).log("Group not found"); + } + } + for (TimeSeriesCategory category : categoriesToCleanup) { + try { + categoryDao.delete(category.getId(), true, category.getOfficeId()); + } catch (NotFoundException e) { + logger.atConfig().withCause(e).log("Category not found"); + } + } + + groupsToCleanup.clear(); + categoriesToCleanup.clear(); + + }, CwmsDataApiSetupCallback.getWebUser()); + } + + + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_create_update_delete(String format) { + String officeId = user.getOperatingOffice(); + String originalId = "test_create_update_delete"; + String updatedId = "test_updated_id"; + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, originalId, "IntegrationTesting"); + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); + String json = Formats.format(contentType, cat); + + // Delete Category to start so its not there. + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, "true") + .when() + .delete("/timeseries/category/" + originalId) + ; // don't care if this fails or not. + + // Verify its not there + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + originalId) + .then() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + categoriesToCleanup.add(cat); + // Create Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(json) + .header("Authorization", user.toHeaderValue()) + .when() + .post("/timeseries/category") + .then() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify it is there + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + originalId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Update Category (Rename and change description) + TimeSeriesCategory updatedCat = new TimeSeriesCategory(officeId, updatedId, "UpdatedDescription"); + categoriesToCleanup.add(updatedCat); + String updatedJson = Formats.format(contentType, updatedCat); + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(updatedJson) + .header("Authorization", user.toHeaderValue()) + .when() + .patch("/timeseries/category/" + originalId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Read and verify update + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + updatedId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(updatedCat.getOfficeId())) + .body("id", equalTo(updatedCat.getId())) + .body("description", equalTo(updatedCat.getDescription())); + + // Verify old ID is gone + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + originalId) + .then() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + // Delete Updated Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, "true") + .when() + .delete("/timeseries/category/" + updatedId) + .then() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Verify new ID is gone + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + updatedId) + .then() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + @ParameterizedTest @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) void test_create_read_delete(String format) { @@ -58,6 +228,7 @@ void test_create_read_delete(String format) { TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_create_read_delete", "IntegrationTesting"); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String xml = Formats.format(contentType, cat); + categoriesToCleanup.add(cat); //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -130,6 +301,8 @@ void test_create_read_delete_new_LRTS_identifier(String format) { TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_lrts_id", "IntegrationTesting"); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String xml = Formats.format(contentType, cat); + + categoriesToCleanup.add(cat); //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -203,6 +376,7 @@ void test_create_already_existing_CWMS_category(String format) { TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "Default", "Default"); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String xml = Formats.format(contentType, cat); + //Attempt to Create Category, should fail given() .log().ifValidationFails(LogDetail.ALL,true) @@ -245,6 +419,8 @@ void test_create_read_delete_same_category_different_office(String format) throw createTimeseries(officeId,timeSeriesId); TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_create_read_delete1", "IntegrationTesting"); TimeSeriesCategory cat2 = new TimeSeriesCategory(officeId2, "test_create_read_delete1", "IntegrationTesting"); + categoriesToCleanup.add(cat); + categoriesToCleanup.add(cat2); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String xml = Formats.format(contentType, cat); //Create Category @@ -373,4 +549,94 @@ void test_create_read_delete_same_category_different_office(String format) throw .assertThat() .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_create_update_ignore_nulls(String format) { + String officeId = user.getOperatingOffice(); + String catId = "test_ignore_nulls"; + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, catId, "InitialDescription"); + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); + String json = Formats.format(contentType, cat); + + categoriesToCleanup.add(cat); + // Create Category with ignore-nulls=false + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(IGNORE_NULLS, "false") + .body(json) + .header("Authorization", user.toHeaderValue()) + .when() + .post("/timeseries/category") + .then() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Update Category with ignore-nulls=true (default) and partial data + // We use a JSON string to send nulls directly + + String partialUpdateJson = "{\"office-id\":\"" + officeId + "\", \"id\":\"" + catId + "\", \"description\":null}"; + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(IGNORE_NULLS, "true") + .body(partialUpdateJson) + .header("Authorization", user.toHeaderValue()) + .when() + .patch("/timeseries/category/" + catId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Verify description was NOT updated to null because ignore-nulls=true + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + catId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("InitialDescription")); + + // Update Category with ignore-nulls=false and null description + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(IGNORE_NULLS, "false") + .body(partialUpdateJson) + .header("Authorization", user.toHeaderValue()) + .when() + .patch("/timeseries/category/" + catId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)); + + // Verify description WAS updated to null + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .when() + .get("/timeseries/category/" + catId) + .then() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", is(org.hamcrest.Matchers.anyOf(equalTo(""), org.hamcrest.Matchers.nullValue()))); + + // Delete Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CASCADE_DELETE, "true") + .when() + .delete("/timeseries/category/" + catId) + .then() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesFilteredControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesFilteredControllerTestIT.java index 13befcf7df..4c9d43a0bb 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesFilteredControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesFilteredControllerTestIT.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.flogger.FluentLogger; import cwms.cda.formatters.Formats; import fixtures.TestAccounts; import io.restassured.RestAssured; @@ -28,14 +29,15 @@ @Tag("integration") class TimeSeriesFilteredControllerTestIT extends DataApiTestIT { + static FluentLogger logger = FluentLogger.forEnclosingClass(); + public static final String JSON_FILE = "/cwms/cda/api/lrl/1hour.json"; @ParameterizedTest @ValueSource(strings = {Formats.JSONV2, Formats.DEFAULT}) void test_filter_nulls(String format) throws Exception { ObjectMapper mapper = new ObjectMapper(); - InputStream resource = this.getClass().getResourceAsStream( - "/cwms/cda/api/lrl/pseudo_reg_1hour.json"); + InputStream resource = this.getClass().getResourceAsStream(JSON_FILE); assertNotNull(resource); String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -74,7 +76,7 @@ void test_filter_nulls(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .when() .redirects().follow(true) .redirects().max(3) @@ -98,7 +100,7 @@ void test_filter_nulls(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .queryParam(Controllers.QUERY,"value!=null") .when() .redirects().follow(true) @@ -110,7 +112,7 @@ void test_filter_nulls(String format) throws Exception { .statusCode(is(HttpServletResponse.SC_OK)) .body("time-series.values[0][0]", equalTo(1673438400000L)) .body("time-series.values[0][1]", closeTo(500.0,0.0001)) - .body("time-series.values[1][0]", equalTo(1673442000000L)) + .body("time-series.values[1][0]", equalTo(1673449200000L)) .body("time-series.values[1][1]", closeTo(600.0,0.0001)) .body("time-series.values.size()", equalTo(2)) ; @@ -124,8 +126,7 @@ void test_filter_nulls(String format) throws Exception { void test_min_value(String format) throws Exception { ObjectMapper mapper = new ObjectMapper(); - InputStream resource = this.getClass().getResourceAsStream( - "/cwms/cda/api/lrl/pseudo_reg_1hour.json"); + InputStream resource = this.getClass().getResourceAsStream(JSON_FILE); assertNotNull(resource); String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -164,7 +165,7 @@ void test_min_value(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .queryParam(Controllers.QUERY, "value>550.0") .when() .redirects().follow(true) @@ -174,7 +175,7 @@ void test_min_value(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)) - .body("time-series.values[0][0]", equalTo(1673442000000L)) + .body("time-series.values[0][0]", equalTo(1673449200000L)) .body("time-series.values[0][1]", closeTo(600.0,0.0001)) .body("time-series.values.size()", equalTo(1)) ; @@ -188,8 +189,7 @@ void test_min_value(String format) throws Exception { void test_max_value(String format) throws Exception { ObjectMapper mapper = new ObjectMapper(); - InputStream resource = this.getClass().getResourceAsStream( - "/cwms/cda/api/lrl/pseudo_reg_1hour.json"); + InputStream resource = this.getClass().getResourceAsStream(JSON_FILE); assertNotNull(resource); String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -228,7 +228,7 @@ void test_max_value(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .queryParam(Controllers.QUERY, "value<=550.0") .when() .redirects().follow(true) @@ -252,8 +252,7 @@ void test_max_value(String format) throws Exception { void test_min_max_value_combined(String format) throws Exception { ObjectMapper mapper = new ObjectMapper(); - InputStream resource = this.getClass().getResourceAsStream( - "/cwms/cda/api/lrl/pseudo_reg_1hour.json"); + InputStream resource = this.getClass().getResourceAsStream(JSON_FILE); assertNotNull(resource); String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -292,7 +291,7 @@ void test_min_max_value_combined(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .queryParam(Controllers.QUERY, "value>450.0 and value <=550.0") .when() .redirects().follow(true) @@ -316,8 +315,7 @@ void test_min_max_value_combined(String format) throws Exception { void test_all_filters_combined(String format) throws Exception { ObjectMapper mapper = new ObjectMapper(); - InputStream resource = this.getClass().getResourceAsStream( - "/cwms/cda/api/lrl/pseudo_reg_1hour.json"); + InputStream resource = this.getClass().getResourceAsStream(JSON_FILE); assertNotNull(resource); String tsData = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -356,7 +354,7 @@ void test_all_filters_combined(String format) throws Exception { .queryParam(Controllers.UNIT,"cfs") .queryParam(Controllers.NAME, ts.get(Controllers.NAME).asText()) .queryParam(Controllers.BEGIN,"2023-01-11T12:00:00-00:00") - .queryParam(Controllers.END,"2023-01-11T13:00:00-00:00") + .queryParam(Controllers.END,"2023-01-11T15:00:00-00:00") .queryParam(Controllers.QUERY, "value!=null and value>450.0 and value <=550.0") .when() .redirects().follow(true) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java index ed2f9a8e17..8d79bf47e0 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesGroupControllerTestIT.java @@ -24,8 +24,34 @@ package cwms.cda.api; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.CASCADE_DELETE; +import static cwms.cda.api.Controllers.CATEGORY_ID; +import static cwms.cda.api.Controllers.CATEGORY_OFFICE_ID; +import static cwms.cda.api.Controllers.CWMS_OFFICE; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.FAIL_IF_EXISTS; +import static cwms.cda.api.Controllers.GROUP_OFFICE_ID; +import static cwms.cda.api.Controllers.IGNORE_NULLS; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.REPLACE_ASSIGNED_LOCS; +import static cwms.cda.api.Controllers.REPLACE_ASSIGNED_TS; +import static cwms.cda.data.dao.Dao.formatBool; +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.flogger.FluentLogger; import cwms.cda.ApiServlet; import cwms.cda.api.errors.NotFoundException; import cwms.cda.data.dao.TimeSeriesCategoryDao; @@ -51,12 +77,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import com.google.common.flogger.FluentLogger; import javax.servlet.http.HttpServletResponse; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; import org.apache.commons.io.IOUtils; import org.hamcrest.Matchers; import org.jooq.Configuration; +import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -67,21 +93,15 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import static cwms.cda.api.Controllers.*; -import static cwms.cda.data.dao.Dao.formatBool; -import static io.restassured.RestAssured.given; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - @Tag("integration") final class TimeSeriesGroupControllerTestIT extends DataApiTestIT { + private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); private final List categoriesToCleanup = new ArrayList<>(); private final List groupsToCleanup = new ArrayList<>(); + + private final List cwmsgroupsToSPKUnassign = new ArrayList<>(); private final List timeSeriesToCleanup = new ArrayList<>(); - private static final FluentLogger LOGGER = FluentLogger.forEnclosingClass(); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; TestAccounts.KeyUser user2 = TestAccounts.KeyUser.SWT_NORMAL; @@ -115,14 +135,37 @@ void clear_data() throws Exception { TimeSeriesCategoryDao categoryDao = new TimeSeriesCategoryDao(configuration.dsl()); TimeSeriesDaoImpl timeSeriesDao = new TimeSeriesDaoImpl(configuration.dsl()); + for (TimeSeriesGroup group : cwmsgroupsToSPKUnassign) { + // We can't delete CWMS groups and we don't want to try to unassign "CWMS" assignments + // We do want to unassign SPK assignments + String assignOffice = user.getOperatingOffice(); + try { + groupDao.unassignForOffice(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), assignOffice ); + } catch (NotFoundException e) { + LOGGER.atConfig().withCause(e).log("Group not found"); + } catch (DataAccessException e) { + LOGGER.atInfo().withCause(e).log("Failed to unassign ts from %s that are in group owned by:%s", assignOffice, group.getOfficeId()); + } + } + cwmsgroupsToSPKUnassign.clear(); + + for (TimeSeriesGroup group : groupsToCleanup) { + String assignOffice = group.getOfficeId(); try { - groupDao.unassignAllTs(group, "SPK"); - if (!group.getOfficeId().equalsIgnoreCase(CWMS_OFFICE)) { - groupDao.delete(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId()); - } + groupDao.unassignForOffice(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), assignOffice); } catch (NotFoundException e) { LOGGER.atConfig().withCause(e).log("Group not found"); + } catch (DataAccessException e) { + LOGGER.atInfo().withCause(e).log("Failed to unassign time series from office %s in group owned by %s", assignOffice, group.getOfficeId() ); + } + + try { + groupDao.delete(group.getTimeSeriesCategory().getId(), group.getId(), group.getOfficeId(), true); + } catch (NotFoundException e) { + LOGGER.atConfig().withCause(e).log("Group not found"); + } catch (DataAccessException e) { + LOGGER.atInfo().withCause(e).log("Failed to delete time series from group in office %s", group.getOfficeId()); } } for (TimeSeriesCategory category : categoriesToCleanup) { @@ -227,138 +270,141 @@ void test_create_read_delete(String format) throws Exception { createLocation(timeSeriesId.split("\\.")[0],true,officeId); TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_create_read_delete", "IntegrationTesting"); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_create_read_delete", "IntegrationTesting", - "sharedTsAliasId", timeSeriesId); + "sharedTsAliasId", timeSeriesId); List assignedTimeSeries = group.getAssignedTimeSeries(); + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); + assignedTimeSeries.add(new AssignedTimeSeries(officeId,timeSeriesId, "AliasId", timeSeriesId, 1)); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); //Create Category given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .body(categoryXml) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, officeId) - .queryParam(FAIL_IF_EXISTS, false) - .when() - .redirects().follow(true) - .redirects().max(3) - .post("/timeseries/category") - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_CREATED)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(categoryXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/category") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); //Create Group given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .body(groupXml) - .header("Authorization", user.toHeaderValue()) - .queryParam(FAIL_IF_EXISTS, false) - .when() - .redirects().follow(true) - .redirects().max(3) - .post("/timeseries/group") - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_CREATED)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); //Read given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .queryParam(OFFICE, officeId) - .queryParam(CATEGORY_OFFICE_ID, officeId) - .queryParam(GROUP_OFFICE_ID, officeId) - .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) - .when() - .redirects().follow(true) - .redirects().max(3) - .get("/timeseries/group/" + group.getId()) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_OK)) - .body("office-id", equalTo(group.getOfficeId())) - .body("id", equalTo(group.getId())) - .body("description", equalTo(group.getDescription())) - .body("assigned-time-series[0].timeseries-id", equalTo(timeSeriesId)) - .body("assigned-time-series[0].alias-id", equalTo("AliasId")) - .body("assigned-time-series[0].ref-ts-id", equalTo(timeSeriesId)) - .body("assigned-time-series[0].ts-code", nullValue()); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("office-id", equalTo(group.getOfficeId())) + .body("id", equalTo(group.getId())) + .body("description", equalTo(group.getDescription())) + .body("assigned-time-series[0].timeseries-id", equalTo(timeSeriesId)) + .body("assigned-time-series[0].alias-id", equalTo("AliasId")) + .body("assigned-time-series[0].ref-ts-id", equalTo(timeSeriesId)) + .body("assigned-time-series[0].ts-code", nullValue()); //Clear Assigned TS group.getAssignedTimeSeries().clear(); groupXml = Formats.format(contentType, group); given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .body(groupXml) - .header("Authorization", user.toHeaderValue()) - .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) - .queryParam(REPLACE_ASSIGNED_TS, "true") - .queryParam(OFFICE, group.getOfficeId()) - .when() - .redirects().follow(true) - .redirects().max(3) - .patch("/timeseries/group/"+ group.getId()) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_OK)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .body(groupXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) + .queryParam(REPLACE_ASSIGNED_TS, "true") + .queryParam(OFFICE, group.getOfficeId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/timeseries/group/"+ group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)); //Delete Group given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, officeId) - .queryParam(CATEGORY_ID, cat.getId()) - .when() - .redirects().follow(true) - .redirects().max(3) - .delete("/timeseries/group/" + group.getId()) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/group/" + group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); //Read Empty given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .queryParam(OFFICE, officeId) - .queryParam(GROUP_OFFICE_ID, officeId) - .queryParam(CATEGORY_OFFICE_ID, officeId) - .when() - .redirects().follow(true) - .redirects().max(3) - .get("/timeseries/group/" + group.getId()) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); //Delete Category given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(format) - .contentType(Formats.JSON) - .header("Authorization", user.toHeaderValue()) - .queryParam(OFFICE, officeId) - .when() - .redirects().follow(true) - .redirects().max(3) - .delete("/timeseries/category/" + group.getTimeSeriesCategory().getId()) - .then() - .assertThat() - .log().ifValidationFails(LogDetail.ALL,true) - .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + .log().ifValidationFails(LogDetail.ALL,true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/category/" + group.getTimeSeriesCategory().getId()) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } @ParameterizedTest @@ -370,6 +416,10 @@ void test_create_read_delete_LRTS(String format) throws Exception { TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_lrts", "IntegrationTesting"); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_lrts", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); + + groupsToCleanup.add(group); + categoriesToCleanup.add(cat); + List assignedTimeSeries = group.getAssignedTimeSeries(); assignedTimeSeries.add(new AssignedTimeSeries(officeId,timeSeriesId, "AliasId", timeSeriesId, 1)); @@ -472,6 +522,7 @@ void test_create_read_delete_LRTS(String format) throws Exception { .contentType(Formats.JSON) .body(groupXml) .header("Authorization", user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(CATEGORY_ID, group.getTimeSeriesCategory().getId()) .queryParam(REPLACE_ASSIGNED_TS, "true") .queryParam(OFFICE, group.getOfficeId()) @@ -483,11 +534,13 @@ void test_create_read_delete_LRTS(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)); + //Delete timeseries given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .header("Authorization", user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(OFFICE, officeId) .queryParam(BEGIN, "2025-05-08T11:00:00+00:00") .queryParam(END, "2025-05-19T11:00:00+00:00") @@ -502,12 +555,14 @@ void test_create_read_delete_LRTS(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_OK)); + //Delete Group given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) .contentType(Formats.JSON) .header("Authorization", user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(OFFICE, officeId) .queryParam(CATEGORY_ID, cat.getId()) .when() @@ -523,6 +578,7 @@ void test_create_read_delete_LRTS(String format) throws Exception { given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) + .header(ApiServlet.IS_NEW_LRTS, true) .contentType(Formats.JSON) .queryParam(OFFICE, officeId) .queryParam(GROUP_OFFICE_ID, officeId) @@ -540,6 +596,7 @@ void test_create_read_delete_LRTS(String format) throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .accept(format) .contentType(Formats.JSON) + .header(ApiServlet.IS_NEW_LRTS, true) .header("Authorization", user.toHeaderValue()) .queryParam(OFFICE, officeId) .when() @@ -907,8 +964,10 @@ void test_rename_group(String format) throws Exception { createTimeseries(officeId, timeSeriesId); TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_rename_group_cat", "IntegrationTesting"); + categoriesToCleanup.add(cat); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_rename_group", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); + groupsToCleanup.add(group); List assignedTimeSeries = group.getAssignedTimeSeries(); assignedTimeSeries.add(new AssignedTimeSeries(officeId,timeSeriesId, "AliasId", timeSeriesId, 1)); @@ -932,7 +991,7 @@ void test_rename_group(String format) throws Exception { .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); - categoriesToCleanup.add(cat); + //Create Group given() .log().ifValidationFails(LogDetail.ALL,true) @@ -949,7 +1008,7 @@ void test_rename_group(String format) throws Exception { .assertThat() .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); - groupsToCleanup.add(group); + TimeSeriesGroup newGroup = new TimeSeriesGroup(cat, officeId, "test_rename_group_new", "IntegrationTesting", "sharedTsAliasId2", timeSeriesId); String newGroupXml = Formats.format(contentType, newGroup); @@ -1054,8 +1113,10 @@ void test_add_assigned_locs(String format) { String officeId = user.getOperatingOffice(); String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_add_assigned_locs", "IntegrationTesting"); + categoriesToCleanup.add(cat); TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_add_assigned_locs", "IntegrationTesting", "sharedTsAliasId", timeSeriesId); + groupsToCleanup.add(group); List assignedTimeSeries = group.getAssignedTimeSeries(); assignedTimeSeries.add(new AssignedTimeSeries(officeId, timeSeriesId, "AliasId", timeSeriesId, 1)); @@ -1193,6 +1254,7 @@ void test_add_assigned_locs(String format) { .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + @Disabled("CWMS_TS.UNASSIGN_TS_GROUP doesn't work for CWMS owned groups") // https://github.com/USACE/cwms-data-api/issues/1631 @ParameterizedTest @ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT}) void test_patch_permissions_CWMS(String format) throws Exception { @@ -1238,19 +1300,17 @@ void test_patch_permissions_CWMS(String format) throws Exception { String tsId = ts.get("name").asText(); TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, categoryName, "Default"); TimeSeriesGroup group = new TimeSeriesGroup(category, CWMS_OFFICE, groupId, "All Time Series", null, null); - AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null); - TimeSeriesGroup newGroup = new TimeSeriesGroup(group, Collections.singletonList(assignedTimeSeries)); - String newGroupJson = Formats.format(new ContentType(Formats.JSONV1), newGroup); - - groupsToCleanup.add(newGroup); + cwmsgroupsToSPKUnassign.add(group); +// groupsToCleanup.add(group); // This is a CWMS office. We can't delete it +// categoriesToCleanup.add(category); // This is a CWMS office. We can't delete it - // Retrieve the group and assert it's empty + // Before we try to modify things - make sure there aren't any other SPK assignments given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .contentType(Formats.JSONV1) - .queryParam(OFFICE, CWMS_OFFICE) // office + .queryParam(OFFICE, officeId) // This param will get us only SPK assignments .queryParam(CATEGORY_OFFICE_ID, CWMS_OFFICE) .queryParam(GROUP_OFFICE_ID, CWMS_OFFICE) .when() @@ -1264,12 +1324,17 @@ void test_patch_permissions_CWMS(String format) throws Exception { .body("description", equalTo("All Time Series")) .body("assigned-time-series.size()", equalTo(0)); + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null); + TimeSeriesGroup newGroup = new TimeSeriesGroup(group, Collections.singletonList(assignedTimeSeries)); + + String newGroupJson = Formats.format(new ContentType(Formats.JSONV1), newGroup); + // Attempt a patch on TS owned by CWMS given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .contentType(Formats.JSONV1) - .header("Authorization", user.toHeaderValue()) + .header("Authorization", user2.toHeaderValue()) .queryParam(OFFICE, officeId) .body(newGroupJson) .when() @@ -1302,6 +1367,7 @@ void test_patch_permissions_CWMS(String format) throws Exception { .body("assigned-time-series[0].timeseries-id", equalTo(tsId)); } + @Disabled("CWMS_TS.UNASSIGN_TS_GROUP doesn't work for CWMS owned groups") // https://github.com/USACE/cwms-data-api/issues/1631 @ParameterizedTest @ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT}) void test_patch_permissions_CWMS_with_replacement(String format) throws Exception { @@ -1369,13 +1435,17 @@ void test_patch_permissions_CWMS_with_replacement(String format) throws Exceptio String tsId2 = ts2.get("name").asText(); TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, categoryName, "Default"); TimeSeriesGroup group = new TimeSeriesGroup(category, CWMS_OFFICE, groupId, "All Time Series", null, null); + + cwmsgroupsToSPKUnassign.add(group); // can't delete CWMS groups but we should be able to unassign SPK assignments +// categoriesToCleanup.add(category); // can't delete CWMS categories + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null); AssignedTimeSeries assignedTimeSeries2 = new AssignedTimeSeries(officeId, tsId2, null, null, null); TimeSeriesGroup newGroup = new TimeSeriesGroup(group, Arrays.asList(assignedTimeSeries2, assignedTimeSeries)); String newGroupJson2 = Formats.format(new ContentType(Formats.JSONV1), newGroup); - groupsToCleanup.add(newGroup); +// groupsToCleanup.add(newGroup); // can't delete CWMS groups // Attempt a patch on TS owned by CWMS with replacement given() @@ -1415,6 +1485,7 @@ void test_patch_permissions_CWMS_with_replacement(String format) throws Exceptio .body("assigned-time-series.size()", equalTo(2)); } + @ParameterizedTest @ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT}) void test_patch_district_permission(String format) throws Exception { @@ -1439,14 +1510,51 @@ void test_patch_district_permission(String format) throws Exception { String tsId = ts.get("name").asText(); TimeSeriesCategory category = new TimeSeriesCategory(CWMS_OFFICE, "Default", "Default"); - TimeSeriesGroup districtGroup = new TimeSeriesGroup(category, CWMS_OFFICE, "Default", "All Time Series", null, null); - AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null); - TimeSeriesGroup newDistrictGroup = new TimeSeriesGroup(districtGroup, Collections.singletonList(assignedTimeSeries)); - groupsToCleanup.add(newDistrictGroup); + // also adds to cleanup +// categoriesToCleanup.add(category); // cwms category, can't delete it. - String newDistrictGroupJson = Formats.format(new ContentType(Formats.JSONV1), newDistrictGroup); + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); + String json = Formats.format(contentType, category); - // inserting the time series + // Create Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json) + .header("Authorization", user.toHeaderValue()) + .when() + .post("/timeseries/category") + .then() + .statusCode(anyOf(is(HttpServletResponse.SC_CREATED), is(HttpServletResponse.SC_CONFLICT))) + ; + + TimeSeriesGroup districtGroup = new TimeSeriesGroup(category, CWMS_OFFICE, "Default", "All Time Series", null, null); + + cwmsgroupsToSPKUnassign.add(districtGroup); + + ContentType contentType1 = Formats.parseHeader(Formats.JSON, TimeSeriesGroup.class); + String json1 = Formats.format(contentType1, districtGroup); + + // Create Group + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSON) + .contentType(Formats.JSON) + .body(json1) + .header("Authorization", user.toHeaderValue()) + .when() + .post("/timeseries/group") + .then() + .statusCode(anyOf(is(HttpServletResponse.SC_CREATED), is(HttpServletResponse.SC_CONFLICT))) + ; + + AssignedTimeSeries assignedTimeSeries = new AssignedTimeSeries(officeId, tsId, null, null, null); + TimeSeriesGroup newDistrictGroup = new TimeSeriesGroup(districtGroup, Collections.singletonList(assignedTimeSeries)); + + String newDistrictGroupJson = Formats.format(new ContentType(Formats.JSONV1), newDistrictGroup); + + // inserting the time series given() .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) @@ -1463,12 +1571,12 @@ void test_patch_district_permission(String format) throws Exception { .assertThat() .statusCode(is(HttpServletResponse.SC_OK)); - // Verify the group is empty + // Precondition - Verify the group is empty given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) .contentType(Formats.JSONV1) - .queryParam(OFFICE, CWMS_OFFICE) //office + .queryParam(OFFICE, officeId) //limit retrieved assignments to office .queryParam(GROUP_OFFICE_ID, CWMS_OFFICE) .queryParam(CATEGORY_OFFICE_ID, CWMS_OFFICE) .when() @@ -1531,6 +1639,8 @@ void testRetrieveOfficeParams(String format) throws Exception { "sharedTsAliasId", timeSeriesId); List assignedTimeSeries = group.getAssignedTimeSeries(); + groupsToCleanup.add(group); + assignedTimeSeries.add(new AssignedTimeSeries(officeId,timeSeriesId, "AliasId", timeSeriesId, 1)); ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); String groupXml = Formats.format(contentType, group); @@ -1688,6 +1798,9 @@ void test_create_read_delete_null_attributes(String format) throws Exception { String categoryXml = Formats.format(contentType, cat); String groupXml = Formats.format(contentType, group); + categoriesToCleanup.add(cat); + groupsToCleanup.add(group); + //Create Category given() .log().ifValidationFails(LogDetail.ALL,true) @@ -1849,4 +1962,424 @@ void test_create_read_delete_null_attributes(String format) throws Exception { .assertThat() .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_delete_respects_cascade_delete_query_param(String format) throws Exception { + String officeId = user.getOperatingOffice(); + String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; + + // Ensure TS exists (load_data usually covers this, but this makes the test resilient) + createLocation(timeSeriesId.split("\\.")[0], true, officeId); + createTimeseries(officeId, timeSeriesId); + + String suffix = String.valueOf(System.currentTimeMillis()); + suffix = suffix.substring(suffix.length() - 6); + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, "test_c_d_" + suffix, "IntegrationTesting"); + TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, "test_c_d_" + suffix, "IntegrationTesting", + "sharedTsAliasId", timeSeriesId); + + group.getAssignedTimeSeries().add(new AssignedTimeSeries(officeId, timeSeriesId, "AliasId", timeSeriesId, 1)); + + // Let @AfterEach clean up if we fail mid-test + categoriesToCleanup.add(cat); + groupsToCleanup.add(group); + + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); + String categoryBody = Formats.format(contentType, cat); + String groupBody = Formats.format(contentType, group); + + // Create Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(categoryBody) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/category") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Group (with an assigned time series) + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(groupBody) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Sanity check: group exists and has an assignment + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("assigned-time-series.size()", greaterThan(0)); + + // Delete WITHOUT cascade_delete: should NOT remove the group (assignments still exist) + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(not(is(HttpServletResponse.SC_NO_CONTENT))); + + // Still exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .body("assigned-time-series.size()", greaterThan(0)) + .statusCode(is(HttpServletResponse.SC_OK)); + + // Delete WITH cascade_delete=true: should succeed even with assignments present + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .queryParam(CASCADE_DELETE, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + + // Now it's gone + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + group.getId()) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @ParameterizedTest + @ValueSource(strings = {Formats.JSON, Formats.DEFAULT}) + void test_create_with_ignore_nulls_parameter(String format) throws Exception { + String officeId = user.getOperatingOffice(); + String timeSeriesId = "Alder Springs.Precip-Cumulative.Inst.15Minutes.0.raw-cda"; + createLocation(timeSeriesId.split("\\.")[0], true, officeId); + + String suffix = String.valueOf(System.currentTimeMillis()); + suffix = suffix.substring(suffix.length() - 6); + String catId = "test_ignore_nulls_" + suffix; + String groupId = "test_ignore_nulls_" + suffix; + + TimeSeriesCategory cat = new TimeSeriesCategory(officeId, catId, "Initial category description"); + TimeSeriesGroup group = new TimeSeriesGroup(cat, officeId, groupId, "Initial group description", + "sharedTsAliasId", timeSeriesId); + + // Let @AfterEach clean up if we fail mid-test + categoriesToCleanup.add(cat); + groupsToCleanup.add(group); + + group.getAssignedTimeSeries().add(new AssignedTimeSeries(officeId, timeSeriesId, "AliasId", timeSeriesId, 1)); + + ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); + String categoryJson = Formats.format(contentType, cat); + String groupJson = Formats.format(contentType, group); + + // Create Category + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(categoryJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/category") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Group initially with description and assigned time series + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify initial state + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("Initial group description")) + .body("assigned-time-series.size()", equalTo(1)) + .body("assigned-time-series[0].timeseries-id", equalTo(timeSeriesId)); + + // Re-create with ignore-nulls=true (default) and null description + // Using JSON string to explicitly send null + String nullDescriptionJson = "{\"office-id\":\"" + officeId + "\",\"id\":\"" + groupId + + "\",\"description\":null,\"time-series-category\":{\"office-id\":\"" + officeId + + "\",\"id\":\"" + catId + "\"}}"; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(nullDescriptionJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_NULLS, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify description was NOT updated to null because ignore-nulls=true + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("Initial group description")) + .body("assigned-time-series.size()", equalTo(1)); + + // Re-create with ignore-nulls=false and null description + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(nullDescriptionJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_NULLS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify description WAS updated to null/empty + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", is(org.hamcrest.Matchers.anyOf(equalTo(""), org.hamcrest.Matchers.nullValue()))); + + // Test ignore-nulls=false with empty assigned-time-series list + // Re-create with original description to set it back + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_NULLS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Re-create with ignore-nulls=false and empty assigned-time-series + String emptyAssignedTsJson = "{\"office-id\":\"" + officeId + "\",\"id\":\"" + groupId + + "\",\"description\":\"Updated description\",\"time-series-category\":{\"office-id\":\"" + officeId + + "\",\"id\":\"" + catId + "\"}" + +// ",\"assigned-time-series\":[]" + // empty is different than null. + "}"; + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(emptyAssignedTsJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_NULLS, false) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify assigned time series list was cleared + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("description", equalTo("Updated description")) + .body("assigned-time-series.size()", equalTo(0)); + + // Re-create with ignore-nulls=true (default) and empty assigned-time-series + // Should preserve the empty list since it's not a valid DB empty list + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .body(groupJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(FAIL_IF_EXISTS, false) + .queryParam(IGNORE_NULLS, true) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/timeseries/group") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Verify assigned time series were added back + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(format) + .contentType(Formats.JSON) + .queryParam(OFFICE, officeId) + .queryParam(CATEGORY_OFFICE_ID, officeId) + .queryParam(GROUP_OFFICE_ID, officeId) + .queryParam(CATEGORY_ID, cat.getId()) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/timeseries/group/" + groupId) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("assigned-time-series.size()", equalTo(1)); + + // delete category and group gets done by afterEach. + } + } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesIdentifierDescriptorControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesIdentifierDescriptorControllerTestIT.java index 5d226c9bad..a0e7ee7678 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesIdentifierDescriptorControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesIdentifierDescriptorControllerTestIT.java @@ -81,23 +81,26 @@ final class TimeSeriesIdentifierDescriptorControllerTestIT extends DataApiTestIT void tearDown() { TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; - for (TimeSeriesIdentifierDescriptor ts : tsDescriptors) { - given() - .log().ifValidationFails(LogDetail.ALL,true) - .accept(Formats.JSONV2) - .contentType(Formats.JSONV2) - .queryParam("office", OFFICE) - .queryParam(Controllers.METHOD,JooqDao.DeleteMethod.DELETE_ALL) - .header("Authorization", user.toHeaderValue()) - .when() - .redirects().follow(true) - .redirects().max(3) - .delete("/timeseries/identifier-descriptor/" + ts.getTimeSeriesId()) - .then() - .log().ifValidationFails(LogDetail.ALL,true) - .assertThat() - .statusCode(is(HttpServletResponse.SC_OK)); + for (boolean lrts : new boolean[]{true, false}) + { + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", OFFICE) + .queryParam(Controllers.METHOD,JooqDao.DeleteMethod.DELETE_ALL) + .header("Authorization", user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, lrts) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/timeseries/identifier-descriptor/" + ts.getTimeSeriesId()) + .then() + .log().ifValidationFails(LogDetail.ALL,true) + ; // not asserting anything - this is cleanup, we don't care if it was found or not. + } + } for (TimeSeriesGroup group : tsGroups) { ContentType contentType = Formats.parseHeader(Formats.JSON, TimeSeriesCategory.class); @@ -245,7 +248,7 @@ void test_create_delete(String format) throws JsonProcessingException, SQLExcept void test_create_delete_new_LRTS_identifier(String format) throws JsonProcessingException, SQLException { createLocation("Alder Springs",true,"SPK"); - String likePattern = "Alder Springs\\.Precip-Cumulative\\.Inst\\.12HoursLocal\\.0\\.DescriptorTEST_LRTS*"; + String likePattern = "Alder Springs\\.Precip-Cumulative\\.Inst\\.12HoursLocal\\.0\\.DescriptorTEST_LRTS.*"; // Check that we don't have any ts like this in the catalog. List names = getIdsLike(OFFICE, likePattern); @@ -258,6 +261,8 @@ void test_create_delete_new_LRTS_identifier(String format) throws JsonProcessing TimeSeriesIdentifierDescriptor ts = buildTimeSeriesIdentifierDescriptor(OFFICE, tsId); String serializedTs = om.writeValueAsString(ts); + tsDescriptors.add(ts); + given() .log().ifValidationFails(LogDetail.ALL,true) .accept(format) @@ -276,7 +281,7 @@ void test_create_delete_new_LRTS_identifier(String format) throws JsonProcessing .statusCode(is(HttpServletResponse.SC_CREATED)); // Check that we have the right number of ts like this in the catalog. - names = getIdsLike(OFFICE, likePattern); + names = getIdsLike(OFFICE, likePattern, true); assertFalse(names.isEmpty()); assertEquals(1, names.size()); String name = names.get(0); @@ -290,6 +295,7 @@ void test_create_delete_new_LRTS_identifier(String format) throws JsonProcessing .queryParam("office", OFFICE) .queryParam(Controllers.METHOD,JooqDao.DeleteMethod.DELETE_ALL) .header("Authorization", user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .when() .redirects().follow(true) .redirects().max(3) @@ -300,7 +306,7 @@ void test_create_delete_new_LRTS_identifier(String format) throws JsonProcessing .statusCode(is(HttpServletResponse.SC_OK)); // Check that we don't have any ts like this in the catalog. - names = getIdsLike(OFFICE, likePattern); + names = getIdsLike(OFFICE, likePattern, true); Assertions.assertTrue(names.isEmpty()); // Try to store it again, but this time with the new LRTS flag set to false. @@ -393,6 +399,7 @@ void pagingTest() throws Exception { TimeSeriesIdentifierDescriptor ts = buildTimeSeriesIdentifierDescriptor(OFFICE, tsId); String serializedTs = om.writeValueAsString(ts); + tsDescriptors.add(ts); given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) @@ -408,7 +415,6 @@ void pagingTest() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_CREATED)); - tsDescriptors.add(ts); } // Add TS with differing ID to verify we don't get it back @@ -416,6 +422,7 @@ void pagingTest() throws Exception { TimeSeriesIdentifierDescriptor ts = buildTimeSeriesIdentifierDescriptor(OFFICE, tsId); String serializedTs = om.writeValueAsString(ts); + tsDescriptors.add(ts); given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) @@ -431,7 +438,6 @@ void pagingTest() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_CREATED)); - tsDescriptors.add(ts); // Check that we have the right number of ts like this in the catalog. names = getIdsLike(OFFICE, likePattern); @@ -551,6 +557,7 @@ void testAliasedTimeSeries() throws Exception { TimeSeriesIdentifierDescriptor ts = buildTimeSeriesIdentifierDescriptor(OFFICE, tsId); String serializedTs = om.writeValueAsString(ts); + tsDescriptors.add(ts); given() .log().ifValidationFails(LogDetail.ALL, true) .accept(Formats.JSONV2) @@ -566,7 +573,6 @@ void testAliasedTimeSeries() throws Exception { .log().ifValidationFails(LogDetail.ALL, true) .assertThat() .statusCode(is(HttpServletResponse.SC_CREATED)); - tsDescriptors.add(ts); } // Check that we have the right number of ts like this in the catalog. @@ -691,6 +697,7 @@ void testAliasedLocations() throws Exception { TimeSeriesIdentifierDescriptor ts = buildTimeSeriesIdentifierDescriptor(OFFICE, tsId); String serializedTs = om.writeValueAsString(ts); + tsDescriptors.add(ts); given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) @@ -706,7 +713,6 @@ void testAliasedLocations() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .assertThat() .statusCode(is(HttpServletResponse.SC_CREATED)); - tsDescriptors.add(ts); } // Check that we have the right number of ts like this in the catalog. @@ -831,10 +837,10 @@ private TimeSeriesIdentifierDescriptor buildTimeSeriesIdentifierDescriptor(Strin return builder.build(); } - - - private static List getIdsLike( String officeId, String likePattern) { + return getIdsLike(officeId, likePattern, false); + } + private static List getIdsLike( String officeId, String likePattern, boolean lrtsFlag) { List retval = new ArrayList<>(); int pageSize = 8000; @@ -843,6 +849,7 @@ private static List getIdsLike( String officeId, String likePattern) { given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) + .header(ApiServlet.IS_NEW_LRTS, lrtsFlag) .queryParam(Controllers.PAGE_SIZE, pageSize) .queryParam(Controllers.OFFICE, officeId) .queryParam(Controllers.LIKE, likePattern) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesProfileInstanceControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesProfileInstanceControllerIT.java index 681f1945dc..3a9bc6e506 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesProfileInstanceControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesProfileInstanceControllerIT.java @@ -221,6 +221,7 @@ void setup() throws Exception { .contentType(Formats.JSONV1) .body(tspData3) .header(AUTH_HEADER, user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(FAIL_IF_EXISTS, false) .when() .redirects().follow(true) @@ -1406,11 +1407,10 @@ void test_previous_next_TimeSeriesProfileInstance_Indexed(String format) throws .queryParam(OFFICE, OFFICE_ID) .queryParam(VERSION_DATE, "2024-07-09T12:00:00.00Z") .queryParam(TIMEZONE, "UTC") - .queryParam(START, "2019-09-09T12:45:00.00Z") + .queryParam(START, "2019-09-09T12:49:00.00Z") .queryParam(END, "2019-09-09T14:45:00.00Z") .queryParam(START_TIME_INCLUSIVE, true) .queryParam(PREVIOUS, true) - .queryParam(NEXT, true) .queryParam(END_TIME_INCLUSIVE, true) .queryParam(UNIT, units) .when() @@ -1444,7 +1444,7 @@ void test_previous_next_TimeSeriesProfileInstance_Indexed(String format) throws .queryParam(VERSION_DATE, "2024-07-09T12:00:00.00Z") .queryParam(TIMEZONE, "UTC") .queryParam(START, "2019-09-09T12:45:00.00Z") - .queryParam(END, "2019-09-09T14:45:00.00Z") + .queryParam(END, "2019-09-09T13:15:00.00Z") .queryParam(START_TIME_INCLUSIVE, true) .queryParam(END_TIME_INCLUSIVE, true) .queryParam(NEXT, true) @@ -1465,13 +1465,13 @@ void test_previous_next_TimeSeriesProfileInstance_Indexed(String format) throws .body("time-series-profile.key-parameter", equalTo(tspInstance.getTimeSeriesProfile().getKeyParameter())) .body("time-series-profile.parameter-list.size()", equalTo(2)) - .body("time-series-list.size()", equalTo(26)) + .body("time-series-list.size()", equalTo(25)) .body("time-series-list[\"1568033937000\"].size()", equalTo(2)) .body("time-series-list[\"1568033750000\"].size()", equalTo(2)) .body("time-series-list[\"1568033787000\"].size()", equalTo(2)) ; - // Retrieve instance with both next and previous set to false (should return the first page) + // Retrieve instance with both next and previous set to false and a small time window (should cause data not found error) given() .log().ifValidationFails(LogDetail.ALL, true) .accept(format) @@ -1480,11 +1480,12 @@ void test_previous_next_TimeSeriesProfileInstance_Indexed(String format) throws .queryParam(OFFICE, OFFICE_ID) .queryParam(VERSION_DATE, "2024-07-09T12:00:00.00Z") .queryParam(TIMEZONE, "UTC") - .queryParam(START, "2019-09-09T12:45:00.00Z") - .queryParam(END, "2019-09-09T14:45:00.00Z") + .queryParam(START, "2019-09-09T12:50:00.00Z") + .queryParam(END, "2019-09-09T13:10:00.00Z") .queryParam(START_TIME_INCLUSIVE, true) .queryParam(END_TIME_INCLUSIVE, true) - .queryParam(PREVIOUS, true) + .queryParam(PREVIOUS, false) + .queryParam(NEXT, false) .queryParam(UNIT, units) .when() .redirects().follow(true) @@ -1502,7 +1503,7 @@ void test_previous_next_TimeSeriesProfileInstance_Indexed(String format) throws .body("time-series-profile.key-parameter", equalTo(tspInstance.getTimeSeriesProfile().getKeyParameter())) .body("time-series-profile.parameter-list.size()", equalTo(2)) - .body("time-series-list.size()", equalTo(26)) + .body("time-series-list.size()", equalTo(18)) .body("time-series-list[\"1568033937000\"].size()", equalTo(2)) .body("time-series-list[\"1568033750000\"].size()", equalTo(2)) .body("time-series-list[\"1568033787000\"].size()", equalTo(2)) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java index 027ee52314..00336f7949 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeSeriesRecentControllerIT.java @@ -102,8 +102,7 @@ static void cleanup() throws Exception { LOGGER.atConfig().log("TimeSeries not found"); } try { - tsGroupDao.unassignAllTs(group, OFFICE_ID); - tsGroupDao.delete(CATEGORY_ID, GROUP_ID, OFFICE_ID); + tsGroupDao.delete(CATEGORY_ID, GROUP_ID, OFFICE_ID, true); } catch (NotFoundException e) { LOGGER.atConfig().log("Group not found"); } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java index 961a5ae92e..9e69627c8d 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/TimeseriesControllerTestIT.java @@ -1,11 +1,29 @@ package cwms.cda.api; -import static cwms.cda.api.Controllers.*; +import static cwms.cda.api.Controllers.BEGIN; +import static cwms.cda.api.Controllers.CREATE_AS_LRTS; +import static cwms.cda.api.Controllers.DATUM; +import static cwms.cda.api.Controllers.END; +import static cwms.cda.api.Controllers.END_TIME_INCLUSIVE; +import static cwms.cda.api.Controllers.INCLUDE_ENTRY_DATE; +import static cwms.cda.api.Controllers.INCLUDE_EXTENTS; +import static cwms.cda.api.Controllers.NAME; +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.OVERRIDE_PROTECTION; +import static cwms.cda.api.Controllers.START_TIME_INCLUSIVE; +import static cwms.cda.api.Controllers.TRIM; +import static cwms.cda.api.Controllers.UNIT; +import static cwms.cda.api.Controllers.VERSION_DATE; import static cwms.cda.data.dao.JooqDao.getDslContext; import static helpers.FloatCloseTo.floatCloseTo; import static io.restassured.RestAssured.given; import static io.restassured.config.JsonConfig.jsonConfig; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -19,23 +37,12 @@ import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import cwms.cda.helpers.DatabaseHelpers.SCHEMA_VERSION; -import cwms.cda.helpers.ZoneIdHelper; import fixtures.CwmsDataApiSetupCallback; import fixtures.MinimumSchema; import fixtures.TestAccounts; import io.restassured.RestAssured; import io.restassured.filter.log.LogDetail; import io.restassured.path.json.config.JsonPathConfig; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.servlet.http.HttpServletResponse; - import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import io.restassured.response.ValidatableResponse; @@ -49,6 +56,16 @@ import org.junit.jupiter.params.provider.EnumSource; import usace.cwms.db.jooq.codegen.packages.CWMS_LOC_PACKAGE; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @Tag("integration") final class TimeseriesControllerTestIT extends DataApiTestIT { public static final int MINIMUM_SCHEMA = 999999; @@ -160,6 +177,7 @@ void test_local_regular_new_LRTS_ID() throws Exception { .config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.DOUBLE))) .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) + .header(ApiServlet.IS_NEW_LRTS, true) .queryParam(OFFICE, officeId) .queryParam(UNIT,"F") .queryParam(NAME, ts.get(NAME).asText()) @@ -692,6 +710,7 @@ void test_lrl_timeseries_lrts_reg1week() throws Exception { .contentType(Formats.JSONV2) .body(tsDataPsuedoOff) .header("Authorization",user.toHeaderValue()) + .header(ApiServlet.IS_NEW_LRTS, false) .queryParam("office",officeId) .when() .redirects().follow(true) diff --git a/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java new file mode 100644 index 0000000000..3b211002a1 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/VerticalDatumControllerTestIT.java @@ -0,0 +1,348 @@ +/* + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to do so, subject to the + * following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package cwms.cda.api; + +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import fixtures.TestAccounts; +import fixtures.TestAccounts.KeyUser; +import io.restassured.filter.log.LogDetail; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import java.util.stream.Stream; + +@Tag("integration") +final class VerticalDatumControllerTestIT extends DataApiTestIT { + + private static final String OFFICE_ID = TestAccounts.KeyUser.SPK_NORMAL.getOperatingOffice(); + + private static final String TEST_LOCATION = "VDI_LOC_TEST"; + + @BeforeAll + static void setup() throws Exception { + createLocation(TEST_LOCATION, true, OFFICE_ID); + } + + + @AfterAll + static void cleanup() { + try { + KeyUser user = KeyUser.SPK_NORMAL; + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XML) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.CASCADE_DELETE, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .delete("/locations/" + TEST_LOCATION) + .then() + .log().ifValidationFails(LogDetail.ALL, true); + } catch (Exception ignore) { + } + + try { + // DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XML) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, KeyUser.SPK_NORMAL.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true); + } catch (Exception ignore) { + } + } + + @BeforeEach + void beforeEach() { + try { + // DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XML) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, KeyUser.SPK_NORMAL.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true); + } catch (Exception ignore) { + } + } + + @MethodSource("provideFormats") + @ParameterizedTest + void test_vertical_datum_crud(ContentType contentType) { + // Build a VerticalDatumInfo payload + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(true, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + String vdiPayload = Formats.format(contentType, vdi); + + KeyUser user = KeyUser.SPK_NORMAL; + + // CREATE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // GET + String getBody = + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + .asString(); + + VerticalDatumInfo got = Formats.parseContent(contentType, getBody, VerticalDatumInfo.class); + assertEquals(100.0, got.getElevation(), 0.001); + assertEquals("NGVD-29", got.getNativeDatum()); + + // UPDATE + VerticalDatumInfo vdiUpdated = new VerticalDatumInfo.Builder() + .from(vdi) + .withElevation(101.25) + .build(); + String vdiUpdatedPayload = Formats.format(contentType, vdiUpdated); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiUpdatedPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + //VERIFY UPDATE + String verifyUpdateBody = + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .extract() + .asString(); + + VerticalDatumInfo gotUpdated = Formats.parseContent(contentType, verifyUpdateBody, VerticalDatumInfo.class); + assertEquals(101.25, gotUpdated.getElevation(), 0.001); + assertEquals("NGVD-29", gotUpdated.getNativeDatum()); + + // DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)); + + //VERIFY DELETE + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .queryParam(OFFICE, OFFICE_ID) + .queryParam(Controllers.UNIT, "m") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + @MethodSource("provideFormats") + @ParameterizedTest + void test_create_vertical_datum_already_exists_fails(ContentType contentType) { + // Build a VerticalDatumInfo payload + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(true, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + String vdiPayload = Formats.format(contentType, vdi); + + KeyUser user = KeyUser.SPK_NORMAL; + + // First CREATE should succeed + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Second CREATE with same payload should fail + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CONFLICT)); + } + + @MethodSource("provideFormats") + @ParameterizedTest + void test_update_vertical_datum_not_found_returns_404(ContentType contentType) { + // Build a VerticalDatumInfo payload + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(true, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice(OFFICE_ID) + .withLocation(TEST_LOCATION) + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + String vdiPayload = Formats.format(contentType, vdi); + + KeyUser user = KeyUser.SPK_NORMAL; + // Attempt UPDATE should return 404 Not Found since no VDI exists yet + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(contentType.toString()) + .contentType(contentType.toString()) + .body(vdiPayload) + .queryParam(OFFICE, OFFICE_ID) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/location/" + TEST_LOCATION + "/vertical-datum") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + } + + static Stream provideFormats() { + return Stream.of( + Arguments.of(Formats.parseHeader(Formats.XMLV1, VerticalDatumInfo.class)), + Arguments.of(Formats.parseHeader(Formats.JSONV1, VerticalDatumInfo.class)) + ); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java index e337724c83..5b3d7c205f 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/WaterSupplyAccountingControllerIT.java @@ -33,11 +33,14 @@ import cwms.cda.data.dao.LookupTypeDao; import cwms.cda.data.dao.project.ProjectDao; import cwms.cda.data.dao.watersupply.WaterContractDao; +import cwms.cda.data.dto.CwmsId; import cwms.cda.data.dto.Location; import cwms.cda.data.dto.LookupType; import cwms.cda.data.dto.project.Project; +import cwms.cda.data.dto.watersupply.PumpLocation; import cwms.cda.data.dto.watersupply.PumpType; import cwms.cda.data.dto.watersupply.WaterSupplyAccounting; +import cwms.cda.data.dto.watersupply.WaterSupplyPump; import cwms.cda.data.dto.watersupply.WaterUser; import cwms.cda.data.dto.watersupply.WaterUserContract; import cwms.cda.formatters.ContentType; @@ -46,7 +49,10 @@ import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; import io.restassured.filter.log.LogDetail; +import java.util.HashSet; +import java.util.Set; import mil.army.usace.hec.test.database.CwmsDatabaseContainer; +import org.apache.commons.codec.binary.Base64; import org.jooq.DSLContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -68,6 +74,7 @@ import static cwms.cda.data.dao.DaoTest.getDslContext; import static cwms.cda.security.ApiKeyIdentityProvider.AUTH_HEADER; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -86,12 +93,14 @@ class WaterSupplyAccountingControllerIT extends DataApiTestIT { private static WaterUserContract contract; private static LookupType testTransferType; private static LookupType testContractType; - private static Location pump1; - private static Location pump2; - private static Location pump3; + private static Location pump4; + private static Location pump5; + private static Location pump6; private static final String OFFICE_ID_TEXT = "office-id"; private static final String MESSAGE = "message"; private static final String IDENTIFIER = "identifier"; + private static final Set contractsToDelete = new HashSet<>(); + private static final Set pumpLocations = new HashSet<>(); static { try (InputStream accountStream = WaterSupplyAccounting.class @@ -100,7 +109,7 @@ class WaterSupplyAccountingControllerIT extends DataApiTestIT { .getResourceAsStream("/cwms/cda/api/waterusercontract.json")) { assert accountStream != null; assert contractStream != null; - String contractJson = org.apache.commons.io.IOUtils.toString(contractStream, StandardCharsets.UTF_8); + String contractJson = IOUtils.toString(contractStream, StandardCharsets.UTF_8); contract = Formats.parseContent(new ContentType(Formats.JSONV1), contractJson, WaterUserContract.class); String accountingJson = IOUtils.toString(accountStream, StandardCharsets.UTF_8); waterSupplyAccounting = Formats.parseContent(new ContentType(Formats.JSONV1), @@ -113,12 +122,21 @@ class WaterSupplyAccountingControllerIT extends DataApiTestIT { .withTooltip("Test tooltip") .build(); testContractType = contract.getContractType(); - pump1 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpIn().getName(), + Location pump1 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpIn().getName(), "PUMP"); - pump2 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpOut().getName(), + Location pump2 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpOut().getName(), "PUMP"); - pump3 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpBelow().getName(), + Location pump3 = buildTestLocation(waterSupplyAccounting.getPumpLocations().getPumpBelow().getName(), "PUMP"); + pump4 = buildTestLocation("Lower Pump 3", "PUMP"); + pump5 = buildTestLocation("Inlet Pump 4", "PUMP"); + pump6 = buildTestLocation("Outlet Pump 5", "PUMP"); + pumpLocations.add(pump1); + pumpLocations.add(pump2); + pumpLocations.add(pump3); + pumpLocations.add(pump4); + pumpLocations.add(pump5); + pumpLocations.add(pump6); } catch (Exception e) { LOGGER.atConfig().log("Unable to delete location: %s", e.getMessage()); } @@ -171,23 +189,16 @@ static void setup() throws Exception { } catch (Exception e) { throw new RuntimeException(e); } - try { - locationsDao.storeLocation(pump1, false); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - locationsDao.storeLocation(pump2, false); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - locationsDao.storeLocation(pump3, false); - } catch (IOException e) { - throw new RuntimeException(e); + for (Location pump : pumpLocations) { + try { + locationsDao.storeLocation(pump, false); + } catch (IOException e) { + throw new RuntimeException(e); + } } try { waterContractDao.storeWaterContract(contract, false, true); + contractsToDelete.add(contract); } catch (Exception e) { throw new RuntimeException(e); } @@ -213,11 +224,13 @@ static void cleanup() { LookupTypeDao lookupTypeDao = new LookupTypeDao(ctx); ProjectDao projectDao = new ProjectDao(ctx); WaterContractDao waterContractDao = new WaterContractDao(ctx); - try - { - waterContractDao.deleteWaterContract(contract, DeleteMethod.DELETE_ALL); - } catch (Exception e) { - LOGGER.atConfig().log("Unable to delete water contract: %s", e.getMessage()); + for (WaterUserContract userContract : contractsToDelete) { + try + { + waterContractDao.deleteWaterContract(userContract, DeleteMethod.DELETE_ALL); + } catch (Exception e) { + LOGGER.atConfig().log("Unable to delete water contract: %s", e.getMessage()); + } } try { @@ -239,17 +252,13 @@ static void cleanup() { } catch (Exception e) { LOGGER.atConfig().log("Unable to delete project: %s", e.getMessage()); } - try - { - locationsDao.deleteLocation(pump1.getName(), pump1.getOfficeId(), true); - } catch (Exception e) { - LOGGER.atConfig().log("Unable to delete location: %s", e.getMessage()); - } - try - { - locationsDao.deleteLocation(pump3.getName(), pump3.getOfficeId(), true); - } catch (Exception e) { - LOGGER.atConfig().log("Unable to delete location: %s", e.getMessage()); + for (Location pump : pumpLocations) { + try + { + locationsDao.deleteLocation(pump.getName(), pump.getOfficeId(), true); + } catch (Exception e) { + LOGGER.atConfig().log("Unable to delete location: %s", e.getMessage()); + } } try { @@ -269,6 +278,264 @@ static void cleanup() { } } + @Test + void testCreateRetrieveDeleteWaterAccountingBase64() throws Exception { + // Test Structure + // 1) Create water contract with / in name + // 2) Store pump accounting + // 3) Retrieve pump accounting + // 4) Assert pump accounting is same as created + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String slashContractId = "Sac River/Pumps"; + + WaterSupplyPump pumpBelow = new WaterSupplyPump.Builder() + .withPumpLocation(pump4) + .withPumpType(PumpType.BELOW) + .build(); + WaterSupplyPump pumpIn = new WaterSupplyPump.Builder() + .withPumpLocation(pump5) + .withPumpType(PumpType.IN) + .build(); + WaterSupplyPump pumpOut = new WaterSupplyPump.Builder() + .withPumpLocation(pump6) + .withPumpType(PumpType.OUT) + .build(); + + WaterUserContract slashContract = new WaterUserContract.Builder() + .withContractId(CwmsId.buildCwmsId(OFFICE_ID, slashContractId)) + .withContractEffectiveDate(contract.getContractEffectiveDate()) + .withContractExpirationDate(contract.getContractExpirationDate()) + .withContractType(contract.getContractType()) + .withContractedStorage(contract.getContractedStorage()) + .withFutureUseAllocation(contract.getFutureUseAllocation()) + .withFutureUsePercentActivated(contract.getFutureUsePercentActivated()) + .withInitialUseAllocation(contract.getInitialUseAllocation()) + .withPumpOutBelowLocation(pumpBelow) + .withPumpInLocation(pumpIn) + .withPumpOutLocation(pumpOut) + .withOfficeId(OFFICE_ID) + .withStorageUnitsId(contract.getStorageUnitsId()) + .withWaterUser(contract.getWaterUser()) + .withTotalAllocPercentActivated(contract.getTotalAllocPercentActivated()) + .build(); + + String encodedContractId = Base64.encodeBase64URLSafeString(slashContractId.getBytes(StandardCharsets.UTF_8)); + + WaterUserContract badContract = new WaterUserContract.Builder() + .withContractId(CwmsId.buildCwmsId(OFFICE_ID, encodedContractId)) + .withContractEffectiveDate(contract.getContractEffectiveDate()) + .withContractExpirationDate(contract.getContractExpirationDate()) + .withContractType(contract.getContractType()) + .withContractedStorage(contract.getContractedStorage()) + .withFutureUseAllocation(contract.getFutureUseAllocation()) + .withFutureUsePercentActivated(contract.getFutureUsePercentActivated()) + .withInitialUseAllocation(contract.getInitialUseAllocation()) + .withPumpOutBelowLocation(pumpBelow) + .withPumpInLocation(pumpIn) + .withPumpOutLocation(pumpOut) + .withOfficeId(OFFICE_ID) + .withStorageUnitsId(contract.getStorageUnitsId()) + .withWaterUser(contract.getWaterUser()) + .withTotalAllocPercentActivated(contract.getTotalAllocPercentActivated()) + .build(); + String contractJson = JsonV1.buildObjectMapper().writeValueAsString(slashContract); + + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .body(contractJson) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + + "/water-user/" + contract.getWaterUser().getEntityName() + "/contracts") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + .body(OFFICE_ID_TEXT, equalTo(OFFICE_ID)) + .body(MESSAGE, equalTo("Water Contract Created Successfully")) + .body(IDENTIFIER, equalTo(contract.getWaterUser().getEntityName())); + + contractsToDelete.add(slashContract); + + WaterSupplyAccounting slashAccounting = new WaterSupplyAccounting.Builder() + .withPumpAccounting(waterSupplyAccounting.getPumpAccounting()) + .withContractName(slashContractId) + .withPumpLocations(new PumpLocation.Builder() + .withPumpBelow(CwmsId.buildCwmsId(OFFICE_ID, pump4.getName())) + .withPumpIn(CwmsId.buildCwmsId(OFFICE_ID, pump5.getName())) + .withPumpOut(CwmsId.buildCwmsId(OFFICE_ID, pump6.getName())) + .build()) + .withWaterUser(waterSupplyAccounting.getWaterUser()) + .build(); + + String json = JsonV1.buildObjectMapper().writeValueAsString(slashAccounting); + + // create pump accounting using JSONV1, expect failure + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + slashContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)) + ; + + // create pump accounting using JSONV1, with encoded contract ID, expect pass + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV1) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + encodedContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + contractsToDelete.add(badContract); + + // create pump accounting with JSONV2 + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .accept(Formats.JSONV2) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + encodedContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + .body(OFFICE_ID_TEXT, equalTo(OFFICE_ID)) + .body(MESSAGE, equalTo("The pump accounting entry was created.")) + .body(IDENTIFIER, equalTo(slashContractId)) + ; + + // retrieve pump accounting using JSONV1, expect failure + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .accept(Formats.JSONV1) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + encodedContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + // retrieve pump accounting using JSONV1, unencoded, expect failure + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .accept(Formats.JSONV1) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + slashContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + // retrieve pump accounting using JSONV2, unencoded, expect failure + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .accept(Formats.JSONV2) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + slashContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); + + // retrieve pump accounting + given() + .log().ifValidationFails(LogDetail.ALL, true) + .header(AUTH_HEADER, user.toHeaderValue()) + .accept(Formats.JSONV2) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + encodedContractId + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].contract-name", equalTo(slashAccounting.getContractName())) + .body("[0].water-user.entity-name", equalTo(waterSupplyAccounting.getWaterUser().getEntityName())) + .body("[0].water-user.project-id.name", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getName())) + .body("[0].water-user.project-id.office-id", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getOfficeId())) + .body("[0].water-user.water-right", equalTo(waterSupplyAccounting.getWaterUser().getWaterRight())) + .body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].pump-type[2]", equalTo(String.format("%s", PumpType.IN))) + .body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].transfer-type-display[2]", equalTo(testTransferType.getDisplayValue())) + .body("[0].pump-locations.pump-in.name", equalTo(slashAccounting.getPumpLocations().getPumpIn().getName())) + .body("[0].pump-locations.pump-below.name", equalTo(slashAccounting.getPumpLocations().getPumpBelow().getName())) + .body("[0].pump-locations.pump-out.name", equalTo(slashAccounting.getPumpLocations().getPumpOut().getName())) + ; + } + + @ParameterizedTest @ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT}) void testCreateRetrieveWaterAccounting(String format) throws Exception { @@ -449,6 +716,66 @@ void testStoreRetrieveWithUnits(String format) throws Exception { ; } + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV1, Formats.DEFAULT}) + void testStoreAsCfsThenRetrieveWithUnits(String format) throws Exception { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String json = JsonV1.buildObjectMapper().writeValueAsString(waterSupplyAccounting); + + json = json.replace("cms", "cfs"); + // create pump accounting + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .body(json) + .header(AUTH_HEADER, user.toHeaderValue()) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + contract.getContractId().getName() + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_CREATED)) + ; + + // retrieve pump accounting + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.JSONV1) + .header(AUTH_HEADER, user.toHeaderValue()) + .accept(format) + .queryParam(START_TIME, "2005-04-05T00:00:00Z") + .queryParam(END_TIME, "2335-04-06T00:00:00Z") + .queryParam(START_INCLUSIVE, "true") + .queryParam(END_INCLUSIVE, "true") + .queryParam(ASCENDING, "true") + .queryParam(ROW_LIMIT, 100) + .queryParam(UNIT, "cfs") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/projects/" + OFFICE_ID + "/" + contract.getWaterUser().getProjectId().getName() + "/water-user/" + + contract.getWaterUser().getEntityName() + "/contracts/" + + contract.getContractId().getName() + "/accounting") + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_OK)) + .body("[0].contract-name", equalTo(waterSupplyAccounting.getContractName())) + .body("[0].water-user.entity-name", equalTo(waterSupplyAccounting.getWaterUser().getEntityName())) + .body("[0].water-user.project-id.name", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getName())) + .body("[0].water-user.project-id.office-id", equalTo(waterSupplyAccounting.getWaterUser().getProjectId().getOfficeId())) + .body("[0].water-user.water-right", equalTo(waterSupplyAccounting.getWaterUser().getWaterRight())) + .body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].pump-type[2]", equalTo(String.format("%s", PumpType.IN))) + .body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].flow[2].toDouble()", closeTo(1.0, 0.0001)) + .body("[0].pump-accounting[\"2022-11-20T21:17:28Z\"].transfer-type-display[2]", equalTo(testTransferType.getDisplayValue())) + .body("[0].pump-locations.pump-in.name", equalTo(waterSupplyAccounting.getPumpLocations().getPumpIn().getName())) + ; + } + @Test void testStoreNonExistentTransferType() throws Exception { TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java new file mode 100644 index 0000000000..13e4485fbf --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerPagingIT.java @@ -0,0 +1,108 @@ +/* + * MIT License + * + * Copyright (c) 2025 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.api.rating; + +import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.PAGE; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import cwms.cda.api.DataApiTestIT; +import cwms.cda.formatters.Formats; +import fixtures.TestAccounts; +import hec.data.RatingException; +import hec.data.cwmsRating.RatingSet; +import io.restassured.filter.log.LogDetail; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.cwms.rating.io.jdbc.RatingJdbcFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import usace.cwms.db.jooq.codegen.packages.CWMS_ENV_PACKAGE; + +@Tag("integration") +class RatingSpecControllerPagingIT extends DataApiTestIT { + + @BeforeAll + static void beforeAll() throws Exception { + String locationId = "RatingSpecGetAllPaged"; + String ratingXml = readResourceFile("cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml"); + String officeId = "SPK"; + createLocation(locationId, true, officeId); + connectionAsWebUser(c ->{ + CWMS_ENV_PACKAGE.call_SET_SESSION_OFFICE_ID(dslContext(c).configuration(), "SPK"); + for(int i = 0; i < RatingSpecController.DEFAULT_PAGE_SIZE * 2; i++) { + try { + String xml = ratingXml.replace("{location-id}", locationId) + .replace("{version-template}", "Test" + i); + RatingSet ratingSet = RatingXmlFactory.ratingSet(xml); + RatingJdbcFactory.store(ratingSet, c, true, true); + } catch (RatingException e) { + throw new RuntimeException(e); + } + } + }); + } + + @Test + void test_getAll_paged() { + var response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, "SPK") + .queryParam("rating-id-mask", ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("page-size", equalTo(RatingSpecController.DEFAULT_PAGE_SIZE)) + .body("total", greaterThan(RatingSpecController.DEFAULT_PAGE_SIZE)) + .body("next-page", notNullValue()) + .extract(); + String nextPage = response.path("next-page"); + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .queryParam(OFFICE, "SPK") + .queryParam(PAGE, nextPage) + .queryParam("rating-id-mask", ".*") + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java index 190b6bbb0d..4e41a849e6 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingSpecControllerTestIT.java @@ -37,6 +37,7 @@ import cwms.cda.data.dao.JooqDao; import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.formatters.ContentType; import cwms.cda.formatters.Formats; import javax.servlet.http.HttpServletResponse; @@ -44,9 +45,12 @@ import static cwms.cda.api.Controllers.METHOD; import static cwms.cda.api.Controllers.OFFICE; +import static cwms.cda.api.Controllers.PAGE_SIZE; +import static cwms.cda.api.Controllers.RATING_ID_MASK; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @Tag("integration") @@ -99,22 +103,40 @@ void test_empty_rating_spec() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_CREATED)); + RatingSpec ratingSpec = new RatingSpec(new RatingSpec.Builder().fromRatingSpec(new hec.data.cwmsRating.RatingSpec(specContainer))); + + // Read and verify no failure on missing office ID + Response response = given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .queryParam(PAGE_SIZE, 500) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/metadata"); + JsonPath noOfficePath = new JsonPath(response.asString()); + assertFalse(noOfficePath.getList("rating-metadata").isEmpty()); + boolean foundMatching = IntStream.range(0, noOfficePath.getInt("rating-metadata.size()")) + .mapToObj(i -> noOfficePath.getObject("rating-metadata[" + i + "].rating-spec", RatingSpec.class)) + .anyMatch(s -> s.hashCode() == ratingSpec.hashCode()); + assertTrue(foundMatching); + //Read - Response response = + response = given() .log().ifValidationFails(LogDetail.ALL,true) .accept(Formats.JSONV2) .contentType(Formats.JSONV2) - .queryParam("office", officeId) + .queryParam(OFFICE, officeId) + .queryParam(RATING_ID_MASK, specContainer.specId) .when() .redirects().follow(true) .redirects().max(3) .get("/ratings/metadata"); // then follows - RatingSpec ratingSpec = new RatingSpec(new RatingSpec.Builder().fromRatingSpec(new hec.data.cwmsRating.RatingSpec(specContainer))); JsonPath path = new JsonPath(response.asString()); //get values of JSON array after getting array size - boolean foundMatching = IntStream.range(0, path.getInt("rating-metadata.size()")) + foundMatching = IntStream.range(0, path.getInt("rating-metadata.size()")) .mapToObj(i -> path.getObject("rating-metadata[" + i + "].rating-spec", RatingSpec.class)) .anyMatch(s -> s.hashCode() == ratingSpec.hashCode()); assertTrue(foundMatching); @@ -235,4 +257,283 @@ void test_create_read_delete() throws Exception { .log().ifValidationFails(LogDetail.ALL,true) .statusCode(is(HttpServletResponse.SC_NOT_FOUND)); } + + @Test + void test_create_read_delete_json() throws Exception { + String locationId = "RatingSpecTestJson"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + //Create Template (XML) + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Create Spec (JSON) + RatingSpec ratingSpec = new RatingSpec.Builder() + .withOfficeId(officeId) + .withRatingId(specContainer.specId) + .withTemplateId(specContainer.templateId) + .withLocationId(locationId) + .withInRangeMethod(specContainer.inRangeMethod) + .withOutRangeLowMethod(specContainer.outRangeLowMethod) + .withOutRangeHighMethod(specContainer.outRangeHighMethod) + .withActive(specContainer.active) + .withAutoUpdate(specContainer.autoUpdate) + .withAutoActivate(specContainer.autoActivate) + .withDescription("JSON Test") + .withIndependentRoundingSpecs(RatingSpec.Builder.buildIndependentRoundingSpecs(specContainer.indRoundingSpecs)) + .withDependentRoundingSpec(specContainer.depRoundingSpec) + .withVersion("Production") // pl/sql error if version isn't specified. + .build(); + + ContentType contentType = Formats.parseHeader(Formats.JSONV2, RatingSpec.class); + String specJson = Formats.format(contentType, ratingSpec); + + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .body(specJson) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + //Read + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .queryParam("office", officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("rating-id", equalTo(specContainer.specId)) + .body("office-id", equalTo(specContainer.officeId)) + .body("template-id", equalTo(specContainer.templateId)) + .body("in-range-method", equalTo(specContainer.inRangeMethod)) + .body("out-range-low-method", equalTo(specContainer.outRangeLowMethod)) + .body("out-range-high-method", equalTo(specContainer.outRangeHighMethod)); + + //Delete + given() + .log().ifValidationFails(LogDetail.ALL,true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + @Test + void test_getOne_xml() throws Exception { + String locationId = "RatingSpecTestXml"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create Template + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Read XML + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XMLV2) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(Formats.XMLV2) + .body("rating-spec.rating-id", equalTo(specContainer.specId)) + .body("rating-spec.office-id", equalTo(specContainer.officeId)) + .body("rating-spec.template-id", equalTo(specContainer.templateId)); + + // Delete + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + @Test + void test_getAll_xml() throws Exception { + String locationId = "RatingSpecGetAllXml"; + String officeId = "SPK"; + createLocation(locationId, true, officeId); + String ratingXml = readResourceFile("cwms/cda/api/Zanesville_Stage_Flow_COE_Production.xml"); + RatingSpecContainer specContainer = RatingSpecXmlFactory.ratingSpecContainer(ratingXml); + specContainer.officeId = officeId; + specContainer.specOfficeId = officeId; + specContainer.locationId = locationId; + specContainer.specId = specContainer.specId.replace("Zanesville", locationId); + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Create Template + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Create Spec + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // Read XML via getAll + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.XMLV2) + .queryParam(OFFICE, officeId) + .queryParam("rating-id-mask", specContainer.specId) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(Formats.XMLV2) + .body("rating-specs.specs.rating-spec.rating-id", equalTo(specContainer.specId)) + .body("rating-specs.specs.rating-spec.office-id", equalTo(specContainer.officeId)) + .body("rating-specs.specs.rating-spec.template-id", equalTo(specContainer.templateId)); + + // Delete + given() + .log().ifValidationFails(LogDetail.ALL, true) + .accept(Formats.JSONV2) + .contentType(Formats.JSONV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, officeId) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects().follow(true) + .redirects().max(3) + .delete("/ratings/spec/" + specContainer.specId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } } diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTest.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTest.java deleted file mode 100644 index 1dd831edeb..0000000000 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTest.java +++ /dev/null @@ -1,362 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2025 Hydrologic Engineering Center - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package cwms.cda.api.rating; - -import static io.restassured.RestAssured.given; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.codahale.metrics.MetricRegistry; - -import cwms.cda.data.dao.DaoTest; -import cwms.cda.data.dao.JsonRatingUtils; -import cwms.cda.data.dao.JsonRatingUtilsTest; -import cwms.cda.formatters.Formats; -import fixtures.TestServletInputStream; -import hec.data.cwmsRating.RatingSet; -import io.javalin.core.util.Header; -import io.javalin.http.Context; -import io.javalin.http.HandlerType; -import io.javalin.http.util.ContextUtil; -import io.javalin.plugin.json.JavalinJackson; -import io.javalin.plugin.json.JsonMapperKt; -import io.restassured.response.Response; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.HashMap; -import com.google.common.flogger.FluentLogger; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.sql.DataSource; -import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; -import org.jetbrains.annotations.Nullable; -import org.jooq.tools.jdbc.MockConnection; -import org.jooq.tools.jdbc.MockFileDatabase; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - - -class RatingsControllerTest { - - private static final FluentLogger logger = FluentLogger.forEnclosingClass(); - - private DataSource ds = mock(DataSource.class); - private Connection conn = null; - - - @BeforeEach - public void baseLineDbMocks() throws IOException{ - InputStream stream = RatingsControllerTest.class.getResourceAsStream("/ratings_db.txt"); - assertNotNull(stream); - this.conn = new MockConnection(new MockFileDatabase(stream)); - assertNotNull(this.conn, "Connection is null; something has gone wrong with the fixture setup"); - } - - - // This is only supposed to test that when XML data is posted to create, - // that data is forward to the method deserializeFromXml - @Test - void post_to_create_passed_to_deserializeXml() throws Exception - { - final String testBody = "could be anything"; - - - RatingController controller = spy(new RatingController(new MetricRegistry())); - HttpServletRequest request = mock(HttpServletRequest.class); - HttpServletResponse response = mock(HttpServletResponse.class); - HashMap attributes = new HashMap<>(); - attributes.put(ContextUtil.maxRequestSizeKey,Integer.MAX_VALUE); - attributes.put(JsonMapperKt.JSON_MAPPER_KEY,new JavalinJackson()); - - when(request.getInputStream()).thenReturn(new TestServletInputStream(testBody)); - //Context context = new Context(request,response, attributes); - Context context = ContextUtil.init(request,response,"*",new HashMap<>(), HandlerType.POST,attributes); - context.attribute("database",this.conn); - - when(request.getContentLength()).thenReturn(testBody.length()); - when(request.getAttribute("database")).thenReturn(this.conn); - - assertNotNull( context.attribute("database"), "could not get the connection back as an attribute"); - - when(request.getHeader(Header.ACCEPT)).thenReturn(Formats.XMLV2); - when(request.getContentType()).thenReturn(Formats.XMLV2); - - logger.atInfo().log("Test post_to_create_passed_to_deserializeXml may trigger a RatingException - this is fine."); - try { - controller.create(context); - } catch (IllegalArgumentException e){ - logger.atInfo().log("Test post_to_create_passed_to_deserializeXml caught an IllegalArgumentException - this is fine."); - } - // For this test, it's ok that the server throws a RatingException - // Only want to check that the controller accessed our mock dao in the expected way - verify(controller, times(1)).deserializeRatingSet(testBody, Formats.XML, true); // Curious that it is XML and not XMLv2 - - } - - @Disabled("incomplete") - @Test - void retrieve_create_retrieve_delete_retrieve_json() throws Exception{ - // Do whatever we need to do to startup the server - int port = 7000; - DataSource testDS = buildDataSource(); - String baseUri = "http://localhost:" + port; - //// Todo: there needs to be some sort of Tomcat start here. - try - { - // Read in a resource and build our test data - String resourcePath = "cwms/cda/data/dao/BEAV.Stage_Flow.BASE.PRODUCTION.xml"; - String refRating = JsonRatingUtilsTest.loadResourceAsString(resourcePath); - assertNotNull(refRating); - RatingSet refRatingSet = RatingXmlFactory.ratingSet(refRating); - String office = "SWT"; - - String refSpecId = refRatingSet.getName(); - String refRatingJson = JsonRatingUtils.toJson(refRatingSet); - String newLoc = "TESTLOC"; - - String testSpecId = refSpecId.replace("BEAV", newLoc); - String testRatingJson = refRatingJson.replace("BEAV", newLoc).replace("Beaver", "TestLoc"); - - try - { - // Make sure we can't find the new rating - Response missingReponse = given() - .baseUri(baseUri) - .accept("application/json;version=2") - .param("office", office) - .when() - .get("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(404, missingReponse.statusCode()); - // Cool, it's not there. - - // Now lets create it from json. - Response createReponse = given() - .baseUri(baseUri) - .body(testRatingJson) - .accept("application/json;version=2") - .when() - .post("/ratings") - .then() - .extract() - .response(); - Assertions.assertEquals(200, createReponse.statusCode()); - // Cool, created it. - - // Now lets get it - Response secondGetReponse = given() - .baseUri(baseUri) - .accept("application/json;version=2") - .param("office", office) - .when() - .get("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(200, secondGetReponse.statusCode()); - // Cool, got it. - } - finally - { - // Now lets delete it - Response deleteReponse = given() - .baseUri(baseUri) - .accept("application/json;version=2") - .param("office", office) - .when() - .delete("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(200, deleteReponse.statusCode()); - // Cool its gone. - } - }finally { - // Maybe somesort of Tomcat shutdown here? - } - } - - @Disabled("incomplete") - @Test - void retrieve_create_retrieve_delete_retrieve_xml() throws Exception{ - // Do whatever we need to do to startup the server - int port = 7000; - DataSource testDS = buildDataSource(); - String baseUri = "http://localhost:" + port; - //// Todo: there needs to be some sort of Tomcat start here. - try - { - // Read in a resource and build our test data - String resourcePath = "cwms/cda/data/dao/BEAV.Stage_Flow.BASE.PRODUCTION.xml"; - String refRatingXml = JsonRatingUtilsTest.loadResourceAsString(resourcePath); - assertNotNull(refRatingXml); - RatingSet refRatingSet = RatingXmlFactory.ratingSet(refRatingXml); - String office = "SWT"; - - String refSpecId = refRatingSet.getName(); - - String newLoc = "TESTLOC"; - - String testSpecId = refSpecId.replace("BEAV", newLoc); - String testRatingXml = refRatingXml.replace("BEAV", newLoc).replace("Beaver", "TestLoc"); - - try - { - // Make sure we can't find the new rating - Response missingReponse = given() - .baseUri(baseUri) - .accept("application/xml;version=2") - .param("office",office) - .when() - .get("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(404, missingReponse.statusCode()); - // Cool, it's not there. - - // Now lets create it from json. - Response createReponse = given() - .baseUri(baseUri) - .body(testRatingXml) - .accept("application/xml;version=2") - .when() - .post("/ratings") - .then() - .extract() - .response(); - Assertions.assertEquals(200, createReponse.statusCode()); - // Cool, created it. - - // Now lets get it - Response secondGetReponse = given() - .baseUri(baseUri) - .accept("application/xml;version=2") - .param("office", office) - .when() - .get("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(200, secondGetReponse.statusCode()); - // Cool, got it. - } - finally - { - // Now lets delete it - Response deleteReponse = given() - .baseUri(baseUri) - .accept("application/xml;version=2") - .param("office", office) - .when() - .delete("/ratings/" + testSpecId) - .then() - .extract() - .response(); - Assertions.assertEquals(200, deleteReponse.statusCode()); - // Cool its gone. - } - }finally { - // Maybe somesort of Tomcat shutdown here? - } - } - - - @Nullable - private DataSource buildDataSource() - { - DataSource testDS = new DataSource() - { - @Override - public Connection getConnection() throws SQLException - { - return DaoTest.getConnection(); - } - - @Override - public Connection getConnection(String username, String password) throws SQLException - { - return DaoTest.getConnection(); - } - - @Override - public T unwrap(Class iface) throws SQLException - { - return null; - } - - @Override - public boolean isWrapperFor(Class iface) throws SQLException - { - return false; - } - - @Override - public PrintWriter getLogWriter() throws SQLException - { - return null; - } - - @Override - public void setLogWriter(PrintWriter out) throws SQLException - { - - } - - @Override - public void setLoginTimeout(int seconds) throws SQLException - { - - } - - @Override - public int getLoginTimeout() throws SQLException - { - return 0; - } - - @Override - public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException - { - return null; - } - }; - return testDS; - } - - -} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java index 3a956f82ed..be1360035a 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestIT.java @@ -373,7 +373,7 @@ enum GetAllTest void test_1206_rating_create_json() throws IOException { // example from 1206 but office changed to SPK String body = readResourceFile("cwms/cda/api/spk/ratings_ind.json"); - + body = body.replaceAll("Barren", EXISTING_LOC); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; // Create the set @@ -397,7 +397,7 @@ void test_1206_rating_create_json() throws IOException { void test_1206_rating_create_xml() throws IOException { // example from 1206 but office changed to SPK and converted to xml String body = readResourceFile("cwms/cda/api/spk/ratings_ind.xml"); - + body = body.replaceAll("Barren", EXISTING_LOC); TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; // Create the set @@ -418,4 +418,3 @@ void test_1206_rating_create_xml() throws IOException { } } - diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java new file mode 100644 index 0000000000..1acbb4f584 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsControllerTestVerticalDatumIT.java @@ -0,0 +1,603 @@ +/* + * MIT License + * Copyright (c) 2025 Hydrologic Engineering Center + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.api.rating; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import cwms.cda.api.DataApiTestIT; +import cwms.cda.data.dao.JooqDao; +import cwms.cda.data.dao.VerticalDatum; +import cwms.cda.data.dto.VerticalDatumInfo; +import cwms.cda.formatters.Formats; +import fixtures.TestAccounts; +import hec.data.cwmsRating.AbstractRating; +import hec.data.cwmsRating.RatingSet; +import hec.data.cwmsRating.io.RatingSetContainer; +import hec.data.cwmsRating.io.RatingSpecContainer; +import io.restassured.filter.log.LogDetail; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.io.IOException; +import java.util.stream.Stream; +import javax.servlet.http.HttpServletResponse; +import mil.army.usace.hec.cwms.rating.io.xml.RatingSetContainerXmlFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingSpecXmlFactory; +import mil.army.usace.hec.cwms.rating.io.xml.RatingXmlFactory; +import mil.army.usace.hec.metadata.VerticalDatumContainer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static cwms.cda.api.Controllers.*; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Tag("integration") +class RatingsControllerTestVerticalDatumIT extends DataApiTestIT { + static final String BASE_LOCATION = "RatingDatumTest"; + static final String LOC_WITH_NAVD88 = BASE_LOCATION + "-NAVD88"; + static final String LOC_WITH_NGVD29 = BASE_LOCATION + "-NGVD29"; + static final String TEMPLATE = "Elev;Area.Standard"; + static final String SPK = "SPK"; + + @BeforeAll + static void beforeAll() throws Exception { + //Make sure we always have something. + createLocation(BASE_LOCATION, true, SPK); + createLocationWithVerticalDatum(LOC_WITH_NAVD88, true, SPK, VerticalDatum.NAVD88); + createLocationWithVerticalDatum(LOC_WITH_NGVD29, true, SPK, VerticalDatum.NGVD29); + + String xml = readVerticalDatumRatingXml(BASE_LOCATION); + RatingSetContainer container = RatingSetContainerXmlFactory.ratingSetContainerFromXml(xml); + RatingSpecContainer specContainer = container.ratingSpecContainer; + String templateXml = RatingSpecXmlFactory.toXml(specContainer, "", 0); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String specXml = RatingSpecXmlFactory.toXml(specContainer, "", 0, true); + + createTemplate(templateXml, user); + + createSpec(specXml, user); + } + + @Test + void test_create_with_datum_param_differs_from_location_native_datum() throws Exception { + // Verify RatingsController create (POST /ratings) accepts the datum query parameter + // when the input rating XML does not include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + String xml = stripVerticalDatumInfo(xmlWithDatum); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(VerticalDatum.NAVD88); + + // First create with a datum that doesn't match native datum for location + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) //using xml with no datum info so param is used + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev + offset.getValue(), 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + + @Test + void test_update_datum_not_in_body_uses_param() throws Exception { + // Verify RatingsController update/store (PATCH /ratings/{id}) accepts the datum query parameter + // when the input rating XML does not include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + String xml = stripVerticalDatumInfo(xmlWithDatum); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(VerticalDatum.NAVD88); + + // First POST to ensure the rating exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + + // 3) PATCH with datum = NAVD88 and no datum info in body means we apply offset, so value changes + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + + // 4) retrieve rating and verify value is changed as expected + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev + offset.getValue(), 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + + @Test + void test_update_with_datum_in_body_ignores_param() throws Exception { + // Verify RatingsController update/store (PATCH /ratings/{id}) ignores the datum query parameter + // when the input rating XML does include vertical-datum-info. + + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Build rating XML for BASE_LOCATION and strip datum info + String xmlWithDatum = readVerticalDatumRatingXml(LOC_WITH_NGVD29); + + RatingSet ratingSet = RatingXmlFactory.ratingSet(xmlWithDatum); + String ratingId = ratingSet.getRatingSpec().getRatingSpecId(); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xmlWithDatum); + JsonNode firstIndNode = root + .path("simple-rating") + .path("rating-points") + .path("point") + .get(0) + .path("ind"); + double firstElev = firstIndNode.asDouble(); + + // First POST to ensure the rating exists + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + + // 2) verify elevation is as expected + Response response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + + // 3) PATCH with datum = NAVD88 but it should be ignored since body contains datum info + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xmlWithDatum) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, VerticalDatum.NAVD88) + .when() + .redirects().follow(true) + .redirects().max(3) + .patch("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)); + + // 4) retrieve rating and verify elevation is still as expected + response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .when() + .redirects().follow(true) + .redirects().max(3) + .get("/ratings/" + ratingId); + response.then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .body("ratings.simple-rating.rating-points.point[0].ind.toDouble()", closeTo(firstElev, 0.001)); + + deleteRatingEffectiveDates(user, ratingId); + } + + private static void createSpec(String specXml, TestAccounts.KeyUser user) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(specXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/ratings/spec") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + private static void createTemplate(String templateXml, TestAccounts.KeyUser user) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(templateXml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/ratings/template") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + @AfterAll + static void cleanUp() { + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + + // Delete Template + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(METHOD, JooqDao.DeleteMethod.DELETE_ALL) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .delete("/ratings/template/" + TEMPLATE) + .then() + .log().ifValidationFails(LogDetail.ALL, true) + .assertThat() + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + @MethodSource(value = "provideDatumCombinations") + @ParameterizedTest + void test_vertical_datum_get_all(TestLocationIds locId, TestLocationVerticalDatumData testData) throws Exception { + String xml = readVerticalDatumRatingXml(locId._locationId); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xml); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + vdi = vdi.convertedTo(vdi.getOffsetForDatum(locId._nativeDatum)); + String newVdiXml = xmlMapper.writeValueAsString(vdi); + xml = xml.replaceAll("", newVdiXml); + RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); + AbstractRating originalRating = originalRatingSet.getRatings()[0]; + originalRating.setVerticalDatumContainer(null); + + storeRatingFromXml(xml, user); + + //Request the one rating id we stored, using the getAll endpoint with a query param filter + String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, requestedVerticalDatum) + .queryParam(NAME, ratingId) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .get("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + deleteRatingEffectiveDates(user, ratingId); + } + + @MethodSource(value = "provideDatumCombinations") + @ParameterizedTest + void test_vertical_datum_get_one(TestLocationIds locId, TestLocationVerticalDatumData testData) throws Exception { + //This tests getting a rating with various combinations of native location datum and requested datum + //Storing a rating without any vertical datum info, then requesting it back with various datum requests + String xml = readVerticalDatumRatingXml(locId._locationId); + XmlMapper xmlMapper = new XmlMapper(); + JsonNode root = xmlMapper.readTree(xml); + JsonNode vdiNode = root + .path("simple-rating") + .path("vertical-datum-info"); + VerticalDatumInfo vdi = xmlMapper.treeToValue(vdiNode, VerticalDatumInfo.class); + vdi = vdi.convertedTo(vdi.getOffsetForDatum(locId._nativeDatum)); + String newVdiXml = xmlMapper.writeValueAsString(vdi); + xml = xml.replaceAll("", newVdiXml); + RatingSet originalRatingSet = RatingXmlFactory.ratingSet(xml); + double firstElev = originalRatingSet.getRatings()[0].getValues(0)[0].getIndValue(); + TestAccounts.KeyUser user = TestAccounts.KeyUser.SPK_NORMAL; + String ratingId = originalRatingSet.getRatingSpec().getRatingSpecId(); + AbstractRating originalRating = originalRatingSet.getRatings()[0]; + originalRating.setVerticalDatumContainer(null); + + double expectedFirstElev = firstElev; + if (testData._requestedVerticalDatum == VerticalDatum.NATIVE || testData._requestedVerticalDatum == null || locId._nativeDatum == null) { + expectedFirstElev = firstElev; + } + else if(testData._requestedVerticalDatum != locId._nativeDatum) { + //Need to apply offset + + VerticalDatumInfo.Offset offset = vdi.getOffsetForDatum(testData._requestedVerticalDatum); + //storedValue = NAVD88 + offset + //-> NAVD88 = storedValue - offset + expectedFirstElev = firstElev - offset.getValue(); + } + + storeRatingFromXml(xml, user); + + //Use getOne endpoint to get the rating we just stored + String requestedVerticalDatum = testData._requestedVerticalDatum == null ? "" : testData._requestedVerticalDatum.toString(); + ExtractableResponse response = given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .queryParam(OFFICE, SPK) + .queryParam(DATUM, requestedVerticalDatum) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .get("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_OK)) + .contentType(is(Formats.XMLV2)) + .extract(); + + deleteRatingEffectiveDates(user, ratingId); + + RatingSet receivedRatingSet = RatingXmlFactory.ratingSet(response.body().asString()); + VerticalDatumContainer receivedDatumContainer = receivedRatingSet.getVerticalDatumContainer(); + assertEquals(locId._nativeDatum == null, receivedDatumContainer == null, "Received VerticalDatumContainer presence mismatch. Expected " + (locId._nativeDatum == null ? "null" : "not null")); + + double receivedFirstElev = receivedRatingSet.getRatings()[0].getValues(0)[0].getIndValue(); + + assertEquals(expectedFirstElev, receivedFirstElev, "Unexpected elev value received"); + } + + private static void storeRatingFromXml(String xml, TestAccounts.KeyUser user) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .body(xml) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .post("/ratings") + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_CREATED)); + } + + private static void deleteRatingEffectiveDates(TestAccounts.KeyUser user, String ratingId) { + given() + .log().ifValidationFails(LogDetail.ALL, true) + .contentType(Formats.XMLV2) + .header("Authorization", user.toHeaderValue()) + .queryParam(OFFICE, SPK) + .queryParam(BEGIN, "2000-01-01T00:00:00Z") + .queryParam(END, "2100-01-01T00:00:00Z") + .when() + .redirects() + .follow(true) + .redirects() + .max(3) + .delete("/ratings/" + ratingId) + .then() + .assertThat() + .log().ifValidationFails(LogDetail.ALL, true) + .statusCode(is(HttpServletResponse.SC_NO_CONTENT)); + } + + private static Stream provideDatumCombinations() { + //This provides information for 3 locations: + // - LOC_WITH_NAVD88: native datum NAVD88 + // - LOC_WITH_NGVD29: native datum NGVD29 + //And for each location, we test requesting: + // - null + // - NATIVE + // - NAVD88 + // - NGVD29 + // + //This creates a 2 x 4 matrix of test cases to cover all combinations of these parameters + return Stream.of(TestLocationIds.values()) + .filter(locId -> !BASE_LOCATION.equals(locId._locationId)) + .flatMap(locId -> Stream.of(TestLocationVerticalDatumData.values()) + .map(datum -> Arguments.of(locId, datum))); + } + + + static @NotNull String readVerticalDatumRatingXml(String location) throws IOException { + return readResourceFile("cwms/cda/api/vertical_datum_example_rating.xml").replace("{office-id}", SPK) + .replace("{location}", location); + } + + // Remove the vertical-datum-info element from the rating XML so that the controller must + // rely on the datum query parameter or the location's native datum. + private static String stripVerticalDatumInfo(String xml) { + return xml.replaceAll("(?s)", ""); + } + + private enum TestLocationIds { + BASE(BASE_LOCATION, null), + NAVD88(LOC_WITH_NAVD88, VerticalDatum.NAVD88), + NGVD29(LOC_WITH_NGVD29, VerticalDatum.NGVD29), + ; + + final String _locationId; + final VerticalDatum _nativeDatum; + + TestLocationIds(String locationId, VerticalDatum nativeDatum) { + _locationId = locationId; + _nativeDatum = nativeDatum; + } + } + + private enum TestLocationVerticalDatumData { + NULL(null), + NATIVE(VerticalDatum.NATIVE), + NAVD88(VerticalDatum.NAVD88), + NGVD29(VerticalDatum.NGVD29), + ; + + final VerticalDatum _requestedVerticalDatum; + + TestLocationVerticalDatumData(VerticalDatum requestedVerticalDatum) { + _requestedVerticalDatum = requestedVerticalDatum; + } + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java new file mode 100644 index 0000000000..f19230fb4d --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/api/rating/RatingsVerticalDatumExtractorTest.java @@ -0,0 +1,20 @@ +package cwms.cda.api.rating; + +import cwms.cda.data.dao.RatingsVerticalDatumExtractor; +import cwms.cda.data.dao.VerticalDatum; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RatingsVerticalDatumExtractorTest { + @Test + void testGetVerticalDatum() throws Exception { + String xml = RatingsControllerTestVerticalDatumIT.readVerticalDatumRatingXml(RatingsControllerTestVerticalDatumIT.LOC_WITH_NGVD29); + Optional datum = RatingsVerticalDatumExtractor.getVerticalDatum(xml); + assertTrue(datum.isPresent()); + datum.ifPresent(vd -> assertSame(VerticalDatum.NGVD29, vd)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java b/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java index f699a2620b..ae77361ad2 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/rss/RssHandlerIT.java @@ -37,6 +37,7 @@ import com.google.common.flogger.FluentLogger; import cwms.cda.api.DataApiTestIT; +import cwms.cda.api.enums.MessageQueue; import cwms.cda.formatters.Formats; import fixtures.CwmsDataApiSetupCallback; import fixtures.TestAccounts; @@ -89,7 +90,7 @@ void test_rss_feed_with_pagination() { .when() .redirects().follow(true) .redirects().max(3) - .get("/rss/" + OFFICE_ID + "/status") + .get("/rss/{office}/{name}", OFFICE_ID, MessageQueue.STATUS.value()) .then() .log().ifValidationFails(LogDetail.ALL, true) .assertThat() @@ -157,7 +158,7 @@ void test_rss_feed_unknown_queue() { .when() .redirects().follow(true) .redirects().max(3) - .get("/rss/" + OFFICE_ID + "/answering-machine") + .get("/rss/{office}/{name}", OFFICE_ID, "answering-machine") .then() .log().ifValidationFails(LogDetail.ALL, true) .assertThat() diff --git a/cwms-data-api/src/test/java/cwms/cda/api/users/UserManagementTestIT.java b/cwms-data-api/src/test/java/cwms/cda/api/users/UserManagementTestIT.java index 0aa61184c6..7ceeb7abdb 100644 --- a/cwms-data-api/src/test/java/cwms/cda/api/users/UserManagementTestIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/api/users/UserManagementTestIT.java @@ -3,11 +3,13 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; +import cwms.cda.api.Controllers; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -191,6 +193,100 @@ void test_list_users_pagination(String authType, TestAccounts.KeyUser theUser, R assertTrue(m5hectest.getRoles().get("SWT").contains("TS ID Creator")); } + @ParameterizedTest + @ArgumentsSource(UserSpecSource.class) + @AuthType(user = TestAccounts.KeyUser.SPK_NORMAL2) + void test_list_users_pagination_regex_filter(String authType, TestAccounts.KeyUser theUser, RequestSpecification authSpec) { + final ArrayList users = new ArrayList(); + Users tmp = given() + .log().ifValidationFails(LogDetail.ALL, true) + .spec(authSpec) + .queryParam(Controllers.USERNAME_LIKE, "l2hectest.*") + .queryParam("page-size", 2) + .when() + .get("/users") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.OK.getStatus())) + .extract().as(Users.class); + + users.addAll(tmp.getUsers()); + assertNotNull(tmp.getNextPage(), "Expected multiple pages of results for pagination test with regex filter."); + + while (tmp.getNextPage() != null) { + tmp = given() + .log().ifValidationFails(LogDetail.ALL, true) + .spec(authSpec) + .queryParam("page",tmp.getNextPage()) + .when() + .get("/users") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.OK.getStatus())) + .extract().as(Users.class); + users.addAll(tmp.getUsers()); + } + + assertEquals(tmp.getTotal(), users.size(), "Returned user size does not match provided total."); + final User l2hectest = users.stream().filter(u -> u.getUserName().equals("L2HECTEST")).findFirst().orElse(null); + assertNotNull(l2hectest, "Could not retrieve expected user."); + assertTrue(l2hectest.getRoles().get("SPK").contains("TS ID Creator")); + } + + @ParameterizedTest + @ArgumentsSource(UserSpecSource.class) + @AuthType(user = TestAccounts.KeyUser.SPK_NORMAL2) + void test_list_users_username_regex_filter(String authType, TestAccounts.KeyUser theUser, RequestSpecification authSpec) { + + Users users = given() + .log().ifValidationFails(LogDetail.ALL, true) + .spec(authSpec) + .queryParam(Controllers.USERNAME_LIKE, "*") + .when() + .get("/users") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.OK.getStatus())) + .extract().as(Users.class); + + assertNotNull(users); + assertNotNull(users.getUsers()); + assertFalse(users.getUsers().isEmpty(), "Expected at least one user returned for regex filter"); + + + users = given() + .log().ifValidationFails(LogDetail.ALL, true) + .spec(authSpec) + .queryParam(Controllers.USERNAME_LIKE, "^M5HECTEST$") + .when() + .get("/users") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.OK.getStatus())) + .extract().as(Users.class); + + assertNotNull(users); + assertNotNull(users.getUsers()); + assertFalse(users.getUsers().isEmpty(), "Expected at least one user returned for regex filter"); + // Ensure the filtered list contains only the expected username + users.getUsers().forEach(u -> assertEquals("M5HECTEST", u.getUserName())); + + users = given() + .log().ifValidationFails(LogDetail.ALL, true) + .spec(authSpec) + .queryParam(Controllers.USERNAME_LIKE, "M3DOESNTEXIST") + .when() + .get("/users") + .then() + .log().ifValidationFails(LogDetail.ALL,true) + .statusCode(is(HttpCode.OK.getStatus())) + .extract().as(Users.class); + + assertNotNull(users); + assertNotNull(users.getUsers()); + assertTrue(users.getUsers().isEmpty(), "Expected no users returned for regex filter"); + } + @Test void test_list_users_fails_if_no_auth() { diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/BlobDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/BlobDaoTest.java new file mode 100644 index 0000000000..59e5fc8929 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/BlobDaoTest.java @@ -0,0 +1,33 @@ +package cwms.cda.data.dao; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class BlobDaoTest { + + @Test + void testGetLength() { + // Full range (end=null): offset=0, end=null, totalLength=10 -> length=10 + assertEquals(10L, BlobDao.getLength(0L, null, 10L)); + + // suffix but we expect method to treat end as full end not the suffix version. + assertEquals(10L, BlobDao.getLength(0L, -1L, 10L)); + + assertEquals(1L, BlobDao.getLength(0L, 0L, 10L)); + assertEquals(2L, BlobDao.getLength(0L, 1L, 10L)); + + // Normal range: offset=0, end=9, totalLength=10 -> length=10 + assertEquals(10L, BlobDao.getLength(0L, 9L, 10L)); + + // Sub range from middle: offset=5, end=9, totalLength=10 -> length=5 + assertEquals(5L, BlobDao.getLength(5L, 9L, 10L)); + + // Offset from middle, end is null: offset=50, end=null, totalLength=10 -> length=50 + assertEquals(6L, BlobDao.getLength(4L, null, 10L)); + assertEquals(6L, BlobDao.getLength(4L, 9L, 10L)); + + assertEquals(1L, BlobDao.getLength(-1L, 1L, 10L)); + + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/LocationVerticalDatumConverterTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/LocationVerticalDatumConverterTest.java new file mode 100644 index 0000000000..edabc69087 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/LocationVerticalDatumConverterTest.java @@ -0,0 +1,45 @@ +package cwms.cda.data.dao; + +import cwms.cda.data.dto.Location; +import cwms.cda.data.dto.VerticalDatumInfo; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; + +import static org.junit.jupiter.api.Assertions.*; + +public class LocationVerticalDatumConverterTest { + + @Test + void testConvertVerticalDatumOnLocation() { + VerticalDatumInfo.Offset[] offsets = new VerticalDatumInfo.Offset[] { + new VerticalDatumInfo.Offset(false, "NGVD-29", 0.0), + new VerticalDatumInfo.Offset(false, "NAVD-88", -0.5) + }; + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice("LRL") + .withLocation("TEST_LOCATION") + .withUnit("m") + .withNativeDatum("NGVD-29") + .withElevation(100.0) + .withOffsets(offsets) + .build(); + + Location loc = new Location.Builder("TEST_LOCATION", "SITE", ZoneId.of("UTC"), + 50.0, 50.0, "NGVD29", "LRL") + .withElevationUnits("m") + .withVerticalDatum("NGVD-29") + .withElevation(100.0) + .build(); + + Location converted = LocationVerticalDatumConverter.convertToVerticalDatum(loc, VerticalDatum.NAVD88, vdi); + + assertEquals("NAVD-88", converted.getVerticalDatum()); + assertEquals(99.5, converted.getElevation(), 1e-6); + + // round-trip back to NGVD29 + Location back = LocationVerticalDatumConverter.convertToVerticalDatum(converted, VerticalDatum.NGVD29, vdi); + assertEquals("NGVD-29", back.getVerticalDatum()); + assertEquals(100.0, back.getElevation(), 1e-6); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingDaoTest.java new file mode 100644 index 0000000000..ef7f838520 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingDaoTest.java @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dao; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import cwms.cda.formatters.FormattingException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +final class RatingDaoTest { + + @ParameterizedTest(name = "{index}: extracts office-id \"{1}\"") + @CsvSource( + value = { + "'\n" + + "\n" + + " SWT\n" + + " TENK.%-Opening,Elev;Flow.Standard.Production\n" + + " %-Opening,Elev;Flow.Standard\n" + + " TENK\n" + + " Production\n" + + " LINEAR\n" + + " NEAREST\n" + + " NEAREST\n" + + " true\n" + + " false\n" + + " false\n" + + " false\n" + + " \n" + + " 0\n" + + " 2222233332\n" + + " \n" + + " 2222233332\n" + + " 2021-06-15T20:03:00Z\n" + + "'", + "'\n" + + " Stage;Stage-Corrected\n" + + " Linear\n" + + " \n" + + " \n" + + " Stage\n" + + " LINEAR\n" + + " NULL\n" + + " NULL\n" + + " \n" + + " \n" + + " Stage-Corrected\n" + + " Stream Stage Correction Rating\n" + + " '" + } + ) + void extractOfficeFromXml_extractsOfficeId(String xml) { + assertEquals("SWT", RatingDao.extractOfficeFromXml(xml)); + } + + @Test + void extractOfficeFromXml_throwsIfMissing() { + String xml = ""; + assertThrows(FormattingException.class, () -> RatingDao.extractOfficeFromXml(xml)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java index cb3c9e709c..77b3f1533f 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecDaoTest.java @@ -32,7 +32,8 @@ void testRetrieveRatingSpecs() throws SQLException, JsonProcessingException { DSLContext lrl = getDslContext(getConnection(), OFFICE_ID); RatingSpecDao dao = new RatingSpecDao(lrl); - Collection ratingSpecs = dao.retrieveRatingSpecs(OFFICE_ID, "^ARTH"); + Collection ratingSpecs = dao.retrieveRatingSpecs(null, 1000, OFFICE_ID, "^ARTH") + .getSpecs(); assertNotNull(ratingSpecs); assertFalse(ratingSpecs.isEmpty()); diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java new file mode 100644 index 0000000000..29bc7c5aa1 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/RatingSpecXmlUtilsTest.java @@ -0,0 +1,38 @@ +package cwms.cda.data.dao; + +import com.fasterxml.jackson.core.JsonProcessingException; +import cwms.cda.data.dto.rating.RatingSpec; +import cwms.cda.data.dto.rating.RatingSpecTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RatingSpecXmlUtilsTest { + + @Test + void testToPlSqlXml() throws JsonProcessingException { + RatingSpec spec = RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production"); + String xml = RatingSpecXmlUtils.toPlSqlXml(spec); +// System.out.println("Debug:" + xml); + + assertTrue(xml.contains("version='1.0'") || xml.contains("version=\"1.0\"")); + assertTrue(xml.contains("encoding='UTF-8'") || xml.contains("encoding=\"UTF-8\"")); + assertTrue(xml.contains(""); + assertTrue(xml.contains(""), "office-id should be an attribute of "); + + assertTrue(xml.contains("ARBU.Elev;Stor.Linear.Production")); + assertTrue(xml.contains("")); + assertTrue(xml.contains("")); + + assertFalse(xml.contains("rating-id>")); + assertFalse(xml.contains("independent-rounding-spec")); + assertFalse(xml.contains("dependent-rounding-spec")); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDaoIT.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDaoIT.java index 981c423967..b162d7cff8 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDaoIT.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyAccountingDaoIT.java @@ -450,12 +450,12 @@ private WaterSupplyAccounting buildTestAccountingWithFewerPumps() { private Map> buildTestPumpAccountingList() { Map> retList = new TreeMap<>(); List transfers = new ArrayList<>(); - transfers.add(new PumpTransfer(PumpType.IN, "Conduit", 100.0, "Test Transfer")); - transfers.add(new PumpTransfer(PumpType.OUT, "Pipeline", 200.0, "Emergency Transfer")); + transfers.add(new PumpTransfer(PumpType.IN, "Conduit", 100.0, "cms", "Test Transfer")); + transfers.add(new PumpTransfer(PumpType.OUT, "Pipeline", 200.0, "cms", "Emergency Transfer")); retList.put(Instant.parse("2025-10-01T00:00:00Z"), transfers); transfers.clear(); - transfers.add(new PumpTransfer(PumpType.OUT, "Canal", 300.0, "Test Transfer")); - transfers.add(new PumpTransfer(PumpType.BELOW, "Stream", 400.0, "Emergency Transfer")); + transfers.add(new PumpTransfer(PumpType.OUT, "Canal", 300.0, "cms", "Test Transfer")); + transfers.add(new PumpTransfer(PumpType.BELOW, "Stream", 400.0, "cms", "Emergency Transfer")); retList.put(Instant.parse("2025-10-02T00:00:00Z"), transfers); return retList; } @@ -463,9 +463,9 @@ private Map> buildTestPumpAccountingList() { private Map> buildTestPumpAccountingListWithFewerPumps() { Map> retList = new TreeMap<>(); retList.put(Instant.parse("2025-10-01T00:00:00Z"), - Collections.singletonList(new PumpTransfer(PumpType.IN, "Conduit", 560.0, "Test Transfer"))); + Collections.singletonList(new PumpTransfer(PumpType.IN, "Conduit", 560.0, "cms", "Test Transfer"))); retList.put(Instant.parse("2025-10-02T00:00:00Z"), - Collections.singletonList(new PumpTransfer(PumpType.IN, "Canal", 750.0, "Test Transfer"))); + Collections.singletonList(new PumpTransfer(PumpType.IN, "Canal", 750.0, "cms", "Test Transfer"))); return retList; } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyUtilsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyUtilsTest.java new file mode 100644 index 0000000000..0648bd72d9 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dao/watersupply/WaterSupplyUtilsTest.java @@ -0,0 +1,141 @@ +/* + * + * MIT License + * + * Copyright (c) 2026 Hydrologic Engineering Center + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE + * SOFTWARE. + */ + +package cwms.cda.data.dao.watersupply; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import cwms.cda.data.dto.CwmsId; +import cwms.cda.data.dto.watersupply.PumpLocation; +import cwms.cda.data.dto.watersupply.PumpTransfer; +import cwms.cda.data.dto.watersupply.PumpType; +import cwms.cda.data.dto.watersupply.WaterSupplyAccounting; +import cwms.cda.data.dto.watersupply.WaterUser; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import mil.army.usace.hec.metadata.Parameter; +import mil.army.usace.hec.metadata.UnitUtil; +import org.junit.jupiter.api.Test; + +class WaterSupplyUtilsTest { + + @Test + void convertsCfsFlowsToSi() throws Exception { + String office = "SPK"; + WaterUser user = new WaterUser.Builder() + .withEntityName("Entity") + .withProjectId(new CwmsId.Builder().withOfficeId(office).withName("Project").build()) + .withWaterRight("Right").build(); + PumpLocation locations = new PumpLocation.Builder() + .withPumpIn(CwmsId.buildCwmsId(office, "Pump In")) + .withPumpOut(CwmsId.buildCwmsId(office, "Pump Out")) + .withPumpBelow(CwmsId.buildCwmsId(office, "Pump Below")).build(); + + Map> accountingMap = new TreeMap<>(); + List list = new ArrayList<>(); + list.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "cfs", "Comment")); + list.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.5, "cfs", "Comment")); + accountingMap.put(Instant.parse("2025-10-01T00:00:00Z"), list); + + WaterSupplyAccounting accounting = new WaterSupplyAccounting.Builder() + .withWaterUser(user) + .withContractName("Contract") + .withPumpLocations(locations) + .withPumpAccounting(accountingMap) + .build(); + + //convert to SI + WaterSupplyAccounting converted = WaterSupplyUtils.convertAccountingFlowsToSi(accounting); + + //Units and values converted + String siUnits = Parameter.getParameter(Parameter.PARAMID_FLOW).getUnitsStringForSystem(UnitUtil.SI_ID); + assertNotNull(converted); + assertEquals(accounting.getContractName(), converted.getContractName()); + assertEquals(accounting.getWaterUser(), converted.getWaterUser()); + assertEquals(accounting.getPumpLocations(), converted.getPumpLocations()); + + for (Map.Entry> e : converted.getPumpAccounting().entrySet()) { + for (PumpTransfer pt : e.getValue()) { + assertEquals(siUnits, pt.getFlowUnit()); + } + } + + List convertedList = converted.getPumpAccounting().get(Instant.parse("2025-10-01T00:00:00Z")); + double expected1 = UnitUtil.convertUnits(1.0, "cfs", siUnits); + double expected2 = UnitUtil.convertUnits(2.5, "cfs", siUnits); + assertEquals(expected1, convertedList.get(0).getFlow(), 1e-6); + assertEquals(expected2, convertedList.get(1).getFlow(), 1e-6); + } + + @Test + void convertCmsFlowsToSiDoesNothing() throws Exception { + String office = "SPK"; + WaterUser user = new WaterUser.Builder() + .withEntityName("Entity") + .withProjectId(new CwmsId.Builder().withOfficeId(office).withName("Project").build()) + .withWaterRight("Right").build(); + PumpLocation locations = new PumpLocation.Builder() + .withPumpIn(CwmsId.buildCwmsId(office, "Pump In")) + .withPumpOut(CwmsId.buildCwmsId(office, "Pump Out")) + .withPumpBelow(CwmsId.buildCwmsId(office, "Pump Below")).build(); + + Map> accountingMap = new TreeMap<>(); + List list = new ArrayList<>(); + list.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "cms", "Comment")); + list.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.5, "cms", "Comment")); + accountingMap.put(Instant.parse("2025-10-01T00:00:00Z"), list); + + WaterSupplyAccounting accounting = new WaterSupplyAccounting.Builder() + .withWaterUser(user) + .withContractName("Contract") + .withPumpLocations(locations) + .withPumpAccounting(accountingMap) + .build(); + + //convert to SI + WaterSupplyAccounting converted = WaterSupplyUtils.convertAccountingFlowsToSi(accounting); + + //Units and values unchanged + assertNotNull(converted); + assertEquals(accounting.getContractName(), converted.getContractName()); + assertEquals(accounting.getWaterUser(), converted.getWaterUser()); + assertEquals(accounting.getPumpLocations(), converted.getPumpLocations()); + + for (Map.Entry> e : converted.getPumpAccounting().entrySet()) { + for (PumpTransfer pt : e.getValue()) { + assertEquals("cms", pt.getFlowUnit()); + } + } + + List convertedList = converted.getPumpAccounting().get(Instant.parse("2025-10-01T00:00:00Z")); + assertEquals(1.0, convertedList.get(0).getFlow(), 1e-6); + assertEquals(2.5, convertedList.get(1).getFlow(), 1e-6); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/ParameterLegacyTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/ParameterLegacyTest.java new file mode 100644 index 0000000000..4473c2f62b --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/ParameterLegacyTest.java @@ -0,0 +1,43 @@ +package cwms.cda.data.dto; + +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import cwms.cda.helpers.DTOMatch; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static cwms.cda.helpers.DTOMatch.assertMatch; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ParameterLegacyTest { + + @Test + void test_serialization() { + ParameterLegacy expected = new ParameterLegacy.Builder() + .withAbstractParam("Area") + .withName("Area") + .withOffice("All") + .withDefaultEnglishUnit("ft2") + .withDefaultSiUnit("m2") + .withLongName("Surface Area") + .withDescription("Area of a surface") + .build(); + ContentType contentType = new ContentType(Formats.JSON_LEGACY); + String json = Formats.format(contentType, expected); + ParameterLegacy parsedParam = Formats.parseContent(contentType, json, ParameterLegacy.class); + assertMatch(expected, parsedParam); + } + + @Test + void test_from_resource_file() throws Exception { + InputStream resource = getClass().getClassLoader().getResourceAsStream("cwms/cda/data/dto/parameter_legacy.json"); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + ContentType contentType = new ContentType(Formats.JSON_LEGACY); + ParameterLegacy receivedTzs = Formats.parseContent(contentType, json, ParameterLegacy.class); + assertNotNull(receivedTzs); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoTest.java index 500878aa8e..9c479611d1 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/VerticalDatumInfoTest.java @@ -122,6 +122,21 @@ void test_json_roundtrip() throws JsonProcessingException assertVDIEquals(expected, actual); } + @Test + void test_xml_serialize_attributes() { + VerticalDatumInfo vdi = new VerticalDatumInfo.Builder() + .withOffice("LRL") + .withUnit("m") + .withLocation("Buckhorn") + .withNativeDatum("NGVD-29") + .withLocalDatumName("Local Datum Name") + .withElevation(230.7) + .build(); + String serialized = new XMLv1().format(vdi); + //verify unit and office are attributes not elements since the db expects them as attributes + assertTrue(serialized.contains("")); + assertFalse(xml.contains("")); + assertFalse(xml.contains("")); - } + RatingSpec spec2 = xmlv2.parseContent(xml, RatingSpec.class); + assertNotNull(spec2); + assertEquals(spec, spec2); + } - public static RatingSpec buildRatingSpec(String officeId, String ratingId) - { - RatingSpec retval; + public static RatingSpec buildRatingSpec(String officeId, String ratingId) { + RatingSpec retval; - String templateId = "Elev;Stor.Linear"; - String locId = "ARBU"; - String version = "Production"; - String agency = null; + String templateId = "Elev;Stor.Linear"; + String locId = "ARBU"; + String version = "Production"; + String agency = null; - boolean activeFlag = true; + boolean activeFlag = true; - boolean autoUpdateFlag = false; + boolean autoUpdateFlag = false; - boolean autoActivateFlag = false; + boolean autoActivateFlag = false; - boolean autoMigrateExtFlag = false; - String indRndSpecs = "2222233332"; + boolean autoMigrateExtFlag = false; + String indRndSpecs = "2222233332"; - String depRndSpecs = "2222233332"; - String desc = null; + String depRndSpecs = "2222233332"; + String desc = null; - String dateMethods = "LINEAR,NEAREST,LOWER"; + String dateMethods = "LINEAR,NEAREST,LOWER"; - RatingSpec.Builder builder = new RatingSpec.Builder(); - builder = builder - .withOfficeId(officeId).withRatingId(ratingId) - .withTemplateId(templateId).withLocationId(locId) - .withVersion(version).withSourceAgency(agency) - .withActive(activeFlag).withAutoUpdate(autoUpdateFlag) - .withAutoActivate(autoActivateFlag) - .withAutoMigrateExtension(autoMigrateExtFlag) - .withIndependentRoundingSpecs(buildIndependentRoundingSpecs(indRndSpecs)) - .withDependentRoundingSpec(depRndSpecs).withDescription(desc) - .withDateMethods(dateMethods); - retval = builder.build(); + RatingSpec.Builder builder = new RatingSpec.Builder(); + builder = builder + .withOfficeId(officeId).withRatingId(ratingId) + .withTemplateId(templateId).withLocationId(locId) + .withVersion(version).withSourceAgency(agency) + .withActive(activeFlag).withAutoUpdate(autoUpdateFlag) + .withAutoActivate(autoActivateFlag) + .withAutoMigrateExtension(autoMigrateExtFlag) + .withIndependentRoundingSpecs(buildIndependentRoundingSpecs(indRndSpecs)) + .withDependentRoundingSpec(depRndSpecs).withDescription(desc) + .withDateMethods(dateMethods); + retval = builder.build(); - assertEquals("LINEAR", retval.getOutRangeLowMethod()); - assertEquals("NEAREST", retval.getInRangeMethod()); - assertEquals("LOWER", retval.getOutRangeHighMethod()); + assertEquals("LINEAR", retval.getOutRangeLowMethod()); + assertEquals("NEAREST", retval.getInRangeMethod()); + assertEquals("LOWER", retval.getOutRangeHighMethod()); - RatingSpec testSpec = builder.withInRangeMethod("InRange") - .withOutRangeLowMethod("OutRangeLow") - .withOutRangeHighMethod("OutRangeHigh") - .build(); + RatingSpec testSpec = builder.withInRangeMethod("InRange") + .withOutRangeLowMethod("OutRangeLow") + .withOutRangeHighMethod("OutRangeHigh") + .build(); - assertEquals("OutRangeLow", testSpec.getOutRangeLowMethod()); - assertEquals("InRange", testSpec.getInRangeMethod()); - assertEquals("OutRangeHigh", testSpec.getOutRangeHighMethod()); + assertEquals("OutRangeLow", testSpec.getOutRangeLowMethod()); + assertEquals("InRange", testSpec.getInRangeMethod()); + assertEquals("OutRangeHigh", testSpec.getOutRangeHighMethod()); - return retval; - } + return retval; + } } \ No newline at end of file diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java new file mode 100644 index 0000000000..51778d4f9b --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/rating/RatingSpecsTest.java @@ -0,0 +1,93 @@ +package cwms.cda.data.dto.rating; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasXPath; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import cwms.cda.formatters.ContentType; +import cwms.cda.formatters.Formats; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +class RatingSpecsTest { + + @ParameterizedTest + @CsvSource({ + "json, " + Formats.JSONV2, + "xml, " + Formats.XMLV2 + }) + void testDeserialize(String ext, String format) throws IOException { + InputStream resource = getClass().getResourceAsStream("/cwms/cda/data/dto/rating/rating_specs." + ext); + assertNotNull(resource); + String json = IOUtils.toString(resource, StandardCharsets.UTF_8); + + ContentType type = Formats.parseHeader(format, RatingSpecs.class); + RatingSpecs specs = Formats.parseContent(type, json, RatingSpecs.class); + + assertNotNull(specs); + assertEquals(2, specs.getSpecs().size()); + } + + @Test + void testXmlSerialize() throws ParserConfigurationException, SAXException, IOException { + RatingSpecs specs = buildRatingSpecs(); + String xml = Formats.format(Formats.parseHeader(Formats.XMLV2, RatingSpecs.class), specs); + assertNotNull(xml); + System.out.println("[DEBUG_LOG] xml: " + xml); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(new InputSource(new StringReader(xml))); + + assertThat(doc, hasXPath("/rating-specs/page-size", is("10"))); + assertThat(doc, hasXPath("/rating-specs/total", is("2"))); + + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[1]/office-id", is("SWT"))); + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[1]/rating-id", is("ARBU.Elev;Stor.Linear.Production"))); + + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[2]/office-id", is("SWT"))); + assertThat(doc, hasXPath("/rating-specs/specs/rating-spec[2]/rating-id", is("OBRK.Elev;Stor.Linear.Production"))); + } + + + @ParameterizedTest + @ValueSource(strings = {Formats.JSONV2, Formats.XMLV2, Formats.DEFAULT}) + void testRoundtrip(String format) { + RatingSpecs specs = buildRatingSpecs(); + + ContentType type = Formats.parseHeader(format, RatingSpecs.class); + String xml = Formats.format(type, specs); + assertNotNull(xml); + + RatingSpecs specs2 = Formats.parseContent(type, xml, RatingSpecs.class); + assertNotNull(specs2); + assertEquals(specs, specs2); + } + + private RatingSpecs buildRatingSpecs() { + List specList = new ArrayList<>(); + specList.add(RatingSpecTest.buildRatingSpec("SWT", "ARBU.Elev;Stor.Linear.Production")); + specList.add(RatingSpecTest.buildRatingSpec("SWT", "OBRK.Elev;Stor.Linear.Production")); + + RatingSpecs.Builder builder = new RatingSpecs.Builder(0, 10, 2); + builder.withSpecs(specList); + return builder.build(); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/PumpAccountingTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/PumpAccountingTest.java index e9842f5aba..084cb722b0 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/PumpAccountingTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/PumpAccountingTest.java @@ -56,6 +56,18 @@ void testWaterSupplyPumpAccountingSerializationRoundTrip() { DTOMatch.assertMatch(pumpAccounting, deserialized); } + @Test + void testPumpTransferUnitsPresent() { + WaterSupplyAccounting accounting = buildTestAccounting(); + // Ensure every PumpTransfer has the expected units string + for (Map.Entry> e : accounting.getPumpAccounting().entrySet()) { + for (PumpTransfer pt : e.getValue()) { + assertNotNull(pt.getFlowUnit(), "Flow unit should not be null"); + assertEquals("cms", pt.getFlowUnit(), "Flow unit should be preserved on PumpTransfer"); + } + } + } + @Test void testWaterSupplyPumpAccountingSerializationRoundTripFromFile() throws Exception { WaterSupplyAccounting pumpAccounting = buildTestAccounting(); @@ -76,20 +88,25 @@ void testValidate() { assertDoesNotThrow(pumpAccounting::validate, "Expected validation to pass"); }, () -> { - PumpTransfer pumpTransfer = new PumpTransfer(null, "Test Transfer Type", 1.0, "Test Comment"); + PumpTransfer pumpTransfer = new PumpTransfer(null, "Test Transfer Type", 1.0, "cms", "Test Comment"); assertThrows(FieldException.class, pumpTransfer::validate, "Expected validation to " + "fail due to null pump type"); }, () -> { - PumpTransfer pumpTransfer = new PumpTransfer(PumpType.OUT, "Test Transfer Type 3", null, "Test Comment 3"); + PumpTransfer pumpTransfer = new PumpTransfer(PumpType.OUT, "Test Transfer Type 3", null, "cms", "Test Comment 3"); assertThrows(FieldException.class, pumpTransfer::validate, "Expected validation to " + "fail due to null flow value"); }, () -> { - PumpTransfer pumpTransfer = new PumpTransfer(PumpType.BELOW, null, 4.0, "Test Comment 4"); + PumpTransfer pumpTransfer = new PumpTransfer(PumpType.BELOW, null, 4.0, "cms", "Test Comment 4"); assertThrows(FieldException.class, pumpTransfer::validate, "Expected validation to " + "fail due to null transfer type display value"); }, + () -> { + PumpTransfer pumpTransfer = new PumpTransfer(PumpType.IN, "Test Transfer Type 2", 2.0, null, "Test Comment 2"); + assertThrows(FieldException.class, pumpTransfer::validate, "Expected validation to " + + "fail due to null flow unit"); + }, () -> { WaterSupplyAccounting pumpAccounting = new WaterSupplyAccounting.Builder().withPumpAccounting(buildTestPumpInAccountingList()) .withPumpLocations(null).withContractName("Sacramento River Water Contract") @@ -129,19 +146,19 @@ private WaterSupplyAccounting buildTestAccounting() { private Map> buildTestPumpInAccountingList() { Map> retMap = new TreeMap<>(); List pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "Added water to the system")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.0, "Removed excess water")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 3.0, "Daily water release")); + pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "cms", "Added water to the system")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.0, "cms", "Removed excess water")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 3.0, "cms", "Daily water release")); retMap.put(Instant.ofEpochMilli(1668979048000L), pumpMap); pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 4.0, "Pump transfer for the day")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 5.0, "Excess water transfer")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 6.0, "Water returned to the river")); + pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 4.0, "cms", "Pump transfer for the day")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 5.0, "cms", "Excess water transfer")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 6.0, "cms", "Water returned to the river")); retMap.put(Instant.ofEpochMilli(1669065448000L), pumpMap); pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN,"Pipeline", 7.0, "Pump transfer for the day")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 8.0, "Excess water transfer")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 9.0, "Water returned to the river")); + pumpMap.add(new PumpTransfer(PumpType.IN,"Pipeline", 7.0, "cms", "Pump transfer for the day")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 8.0, "cms", "Excess water transfer")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 9.0, "cms", "Water returned to the river")); retMap.put(Instant.ofEpochMilli(1669151848000L), pumpMap); return retMap; } diff --git a/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/WaterSupplyAccountingTest.java b/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/WaterSupplyAccountingTest.java index cbe7f78b08..ba6eb7740a 100644 --- a/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/WaterSupplyAccountingTest.java +++ b/cwms-data-api/src/test/java/cwms/cda/data/dto/watersupply/WaterSupplyAccountingTest.java @@ -187,19 +187,19 @@ void testValidate() { private Map> buildTestPumpAccountingList() { Map> retMap = new TreeMap<>(); List pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "Added water to the system")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.0, "Removed excess water")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 3.0, "Daily water release")); + pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 1.0, "cms", "Added water to the system")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 2.0, "cms", "Removed excess water")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 3.0, "cms", "Daily water release")); retMap.put(Instant.ofEpochMilli(1668979048000L), pumpMap); pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 4.0, "Pump transfer for the day")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 5.0, "Excess water transfer")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 6.0, "Water returned to the river")); + pumpMap.add(new PumpTransfer(PumpType.IN, "Pipeline", 4.0, "cms", "Pump transfer for the day")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 5.0, "cms", "Excess water transfer")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 6.0, "cms", "Water returned to the river")); retMap.put(Instant.ofEpochMilli(1669065448000L), pumpMap); pumpMap = new ArrayList<>(); - pumpMap.add(new PumpTransfer(PumpType.IN,"Pipeline", 7.0, "Pump transfer for the day")); - pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 8.0, "Excess water transfer")); - pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 9.0, "Water returned to the river")); + pumpMap.add(new PumpTransfer(PumpType.IN,"Pipeline", 7.0, "cms", "Pump transfer for the day")); + pumpMap.add(new PumpTransfer(PumpType.OUT, "Pipeline", 8.0, "cms", "Excess water transfer")); + pumpMap.add(new PumpTransfer(PumpType.BELOW, "River", 9.0, "cms", "Water returned to the river")); retMap.put(Instant.ofEpochMilli(1669151848000L), pumpMap); return retMap; } diff --git a/cwms-data-api/src/test/java/cwms/cda/features/CdaFeatureManagerProviderTest.java b/cwms-data-api/src/test/java/cwms/cda/features/CdaFeatureManagerProviderTest.java new file mode 100644 index 0000000000..7a2f68a2a1 --- /dev/null +++ b/cwms-data-api/src/test/java/cwms/cda/features/CdaFeatureManagerProviderTest.java @@ -0,0 +1,77 @@ +package cwms.cda.features; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.togglz.core.manager.FeatureManager; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; + +class CdaFeatureManagerProviderTest { + + private String originalPropertiesFile; + + @BeforeEach + void setUp() { + originalPropertiesFile = System.getProperty(CdaFeatureManagerProvider.PROPERTIES_FILE); + } + + @AfterEach + void tearDown() { + if (originalPropertiesFile != null) { + System.setProperty(CdaFeatureManagerProvider.PROPERTIES_FILE, originalPropertiesFile); + } else { + System.clearProperty(CdaFeatureManagerProvider.PROPERTIES_FILE); + } + } + + @Test + void testPriority() { + CdaFeatureManagerProvider provider = new CdaFeatureManagerProvider(); + assertEquals(10, provider.priority()); + } + + @Test + void testGetFeatureManager() { + CdaFeatureManagerProvider provider = new CdaFeatureManagerProvider(); + FeatureManager manager = provider.getFeatureManager(); + assertNotNull(manager); + assertSame(manager, provider.getFeatureManager(), "Should return the same instance"); + } + + @Test + void testUseObjectStorageBlobsFeature() throws IOException { + File tempFile = Files.createTempFile("features", ".properties").toFile(); + tempFile.deleteOnExit(); + + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write(CdaFeatures.USE_OBJECT_STORAGE_BLOBS.name() + " = true"); + } + + System.setProperty(CdaFeatureManagerProvider.PROPERTIES_FILE, tempFile.getAbsolutePath()); + + CdaFeatureManagerProvider provider = new CdaFeatureManagerProvider(); + FeatureManager manager = provider.getFeatureManager(); + + assertTrue(manager.isActive(CdaFeatures.USE_OBJECT_STORAGE_BLOBS)); + } + + @Test + void testFeatureDisabledByDefault() throws IOException { + File tempFile = Files.createTempFile("features_disabled", ".properties").toFile(); + tempFile.deleteOnExit(); + + // Empty file should mean features are disabled by default + System.setProperty(CdaFeatureManagerProvider.PROPERTIES_FILE, tempFile.getAbsolutePath()); + + CdaFeatureManagerProvider provider = new CdaFeatureManagerProvider(); + FeatureManager manager = provider.getFeatureManager(); + + assertFalse(manager.isActive(CdaFeatures.USE_OBJECT_STORAGE_BLOBS)); + } +} diff --git a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java index a285ea4773..60067d5afb 100644 --- a/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java +++ b/cwms-data-api/src/test/java/cwms/cda/helpers/DTOMatch.java @@ -26,6 +26,7 @@ import cwms.cda.data.dto.CwmsIdTimeExtentsEntry; import cwms.cda.data.dto.Entity; +import cwms.cda.data.dto.ParameterLegacy; import cwms.cda.data.dto.TimeExtents; import cwms.cda.data.dto.TimeSeriesExtents; import cwms.cda.data.dto.catalog.LocationAlias; @@ -507,6 +508,18 @@ public static void assertMatch(StreamflowMeasurement first, StreamflowMeasuremen ); } + public static void assertMatch(ParameterLegacy first, ParameterLegacy second) { + assertAll( + () -> assertEquals(first.getAbstractParam(), second.getAbstractParam(), "Abstract parameter does not match"), + () -> assertEquals(first.getName(), second.getName(), "Parameter name does not match"), + () -> assertEquals(first.getOffice(), second.getOffice(), "Office does not match"), + () -> assertEquals(first.getDefaultEnglishUnit(), second.getDefaultEnglishUnit(), "Default English unit does not match"), + () -> assertEquals(first.getDefaultSiUnit(), second.getDefaultSiUnit(), "Default SI unit does not match"), + () -> assertEquals(first.getLongName(), second.getLongName(), "Long name does not match"), + () -> assertEquals(first.getDescription(), second.getDescription(), "Description does not match") + ); + } + public static void assertMatch(SupplementalStreamflowMeasurement first, SupplementalStreamflowMeasurement second) { assertAll( () -> assertEquals(first.getChannelFlow(), second.getChannelFlow(), DEFAULT_DELTA, "Channel flow does not match"), diff --git a/cwms-data-api/src/test/java/fixtures/KeyCloakExtension.java b/cwms-data-api/src/test/java/fixtures/KeyCloakExtension.java index 293c4848fc..4949b27186 100644 --- a/cwms-data-api/src/test/java/fixtures/KeyCloakExtension.java +++ b/cwms-data-api/src/test/java/fixtures/KeyCloakExtension.java @@ -38,12 +38,13 @@ public final class KeyCloakExtension implements BeforeAllCallback { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final String WELL_KNOWN = "realms/cwms/.well-known/openid-configuration"; private static final ObjectMapper mapper = new ObjectMapper(); - private static final GenericContainer kcc = new GenericContainer<>("quay.io/keycloak/keycloak:19.0.1") + private static final GenericContainer kcc = new GenericContainer<>("quay.io/keycloak/keycloak:26.5") .withEnv("KC_HTTP_ENABLED", "true") .withEnv("KC_HOSTNAME_STRICT", "false") + .withEnv("KC_LOG_LEVEL", "info") .withEnv("KEYCLOAK_ADMIN","admin") .withEnv("KEYCLOAK_ADMIN_PASSWORD","admin") - .withCommand("start-dev --features-disabled=admin2 --import-realm") + .withCommand("start-dev --import-realm") .withExposedPorts(8080) .withReuse(false) .withLogConsumer(frame -> diff --git a/cwms-data-api/src/test/java/fixtures/MinIOExtension.java b/cwms-data-api/src/test/java/fixtures/MinIOExtension.java new file mode 100644 index 0000000000..15dce3f721 --- /dev/null +++ b/cwms-data-api/src/test/java/fixtures/MinIOExtension.java @@ -0,0 +1,56 @@ +package fixtures; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.MinIOContainer; +import org.testcontainers.junit.jupiter.Container; + +/** + * Sets up a KeyCloak instance to use for testing. + */ +public final class MinIOExtension implements BeforeAllCallback { + + public static final String MINIO_USER = "cda_user"; + public static final String MINIO_USER_SECRET = "cda_password"; + public static final String IMAGE_NAME = "minio/minio:RELEASE.2025-04-22T22-12-26Z"; + public static final int PORT = 9000; + + public static final String BUCKET = "cwms-test"; + + @Container + private static final MinIOContainer MINIO_CONTAINER = new MinIOContainer(IMAGE_NAME) + .withUserName(MINIO_USER) + .withPassword(MINIO_USER_SECRET); + + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + if (!MINIO_CONTAINER.isRunning()) { + MINIO_CONTAINER.start(); + createTestBucket(); + } + + String host = MINIO_CONTAINER.getHost(); + Integer port = MINIO_CONTAINER.getMappedPort(PORT); + + System.setProperty("blob.store.endpoint", "http://" + host + ":" + port); + System.setProperty("blob.store.bucket", BUCKET); + System.setProperty("blob.store.accessKey", MINIO_USER); + System.setProperty("blob.store.secretKey", MINIO_USER_SECRET); + } + + private static void createTestBucket() { + try (var client = io.minio.MinioClient.builder() + .endpoint(MINIO_CONTAINER.getS3URL()) + .credentials(MINIO_USER, MINIO_USER_SECRET) + .build()) { + if (!client.bucketExists(io.minio.BucketExistsArgs.builder().bucket(BUCKET).build())) { + client.makeBucket(io.minio.MakeBucketArgs.builder().bucket(BUCKET).build()); + } + } catch (Exception e) { + throw new RuntimeException("Failed to create test bucket", e); + } + } + + +} diff --git a/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java b/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java index ef85450d6d..38e6feae31 100644 --- a/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java +++ b/cwms-data-api/src/test/java/helpers/OpenApiParamInfo.java @@ -26,6 +26,7 @@ public class OpenApiParamInfo { private String name; private final boolean required; private final Class type; + private boolean ignoreRequired = false; public OpenApiParamInfo(String name, boolean required, Class type) { this.name = name; @@ -38,6 +39,15 @@ public OpenApiParamInfo setName(String name) { return this; } + public OpenApiParamInfo setIgnoreRequired(boolean ignoreRequired) { + this.ignoreRequired = ignoreRequired; + return this; + } + + public boolean ignoreRequired() { + return ignoreRequired; + } + public String getName() { return name; } diff --git a/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java b/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java index 919b2abaad..b3e62d21ac 100644 --- a/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java +++ b/cwms-data-api/src/test/java/helpers/OpenApiTestHelper.java @@ -8,6 +8,7 @@ import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver; import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import cwms.cda.helpers.annotations.IgnoreRequiredQueryParamMismatch; import io.javalin.plugin.openapi.annotations.OpenApi; import io.javalin.plugin.openapi.annotations.OpenApiParam; import java.io.IOException; @@ -47,10 +48,20 @@ public static OpenApiDocInfo readDocParams(Method m) { if (oa == null || oa.ignore()) { return new OpenApiDocInfo(m, true); } + String[] ignored = new String[0]; + IgnoreRequiredQueryParamMismatch ignore = m.getAnnotation(IgnoreRequiredQueryParamMismatch.class); + if (ignore != null) { + ignored = ignore.parameterNames(); + } OpenApiDocInfo info = new OpenApiDocInfo(m, false); for (OpenApiParam p : oa.queryParams()) { if (p != null && !p.name().trim().isEmpty()) { OpenApiParamInfo paramObj = new OpenApiParamInfo(p.name(), p.required(), p.type()); + for (String ignoredName : ignored) { + if (p.name().equalsIgnoreCase(ignoredName)) { + paramObj = paramObj.setIgnoreRequired(true); + } + } info.getQueryParameters().add(paramObj); } } diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml b/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml new file mode 100644 index 0000000000..f5c1346d56 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/RatingSpecGetAllPaged_Stage_Flow_COE_Production.xml @@ -0,0 +1,50 @@ + + + + Stage;Flow + COE + + + Stage + LINEAR + ERROR + ERROR + + + Flow + + + + {location-id}.Stage;Flow.COE.{version-template} + Stage;Flow.COE + {location-id} + {version-template} + + LINEAR + NULL + NEAREST + true + false + false + false + + 4444444444 + + 4444444444 + + + + {location-id}.Stage;Flow.COE.{version-template} + ft;cfs + 2002-04-09T13:53:01Z + 2014-06-11T14:46:00Z + true + + + + 2.37744006 + 14.1584233 + + + + diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/lrl/1hour.json b/cwms-data-api/src/test/resources/cwms/cda/api/lrl/1hour.json new file mode 100644 index 0000000000..1a72a1bd3b --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/lrl/1hour.json @@ -0,0 +1,27 @@ +{ + "name": "Calhoun.Flow.Inst.1Hour.0.cda-test", + "office-id": "SPK", + "units": "cfs", + "values": [ + [ + 1673438400000, + 500, + 0 + ], + [ + 1673442000000, + null, + 5 + ], + [ + 1673445600000, + -340282346638528859811704183484516925440, + 5 + ], + [ + 1673449200000, + 600, + 0 + ] + ] +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting.json b/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting.json index 9f3e5ec789..e9e857058e 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting.json +++ b/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting.json @@ -1,5 +1,5 @@ { - "contract-name": "Sac River", + "contract-name": "Sac River Pumps", "water-user": { "entity-name": "California DWR", "project-id": { @@ -28,18 +28,21 @@ "pump-type": "IN", "transfer-type-display": "Temporary Inlet", "flow": 1.0, + "flow-unit": "cms", "comment": "Added water to the system" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 2.0, + "flow-unit": "cms", "comment": "Removed excess water" }, { "pump-type": "BELOW", "transfer-type-display": "Pipeline", "flow": 3.0, + "flow-unit": "cms", "comment": "Daily water release" } ], @@ -48,18 +51,21 @@ "pump-type": "IN", "transfer-type-display": "Pipeline", "flow": 4.0, + "flow-unit": "cms", "comment": "Pump transfer for the day" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 5.0, + "flow-unit": "cms", "comment": "Excess water transfer" }, { "pump-type": "BELOW", "transfer-type-display": "Pipeline", "flow": 6.0, + "flow-unit": "cms", "comment": "Water returned to the river" } ], @@ -68,18 +74,21 @@ "pump-type": "IN", "transfer-type-display": "Pipeline", "flow": 7.0, + "flow-unit": "cms", "comment": "Pump transfer for the day" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 8.0, + "flow-unit": "cms", "comment": "Excess water transfer" }, { "pump-type": "BELOW", "transfer-type-display": "Pipeline", "flow": 9.0, + "flow-unit": "cms", "comment": "Water returned to the river" } diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting_slash_name.json b/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting_slash_name.json new file mode 100644 index 0000000000..3c5aed346f --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/pump_accounting_slash_name.json @@ -0,0 +1,97 @@ +{ + "contract-name": "Salina Pumpback Generators/Pumps", + "water-user": { + "entity-name": "California DWR", + "project-id": { + "office-id": "SPK", + "name": "Sacramento Delta" + }, + "water-right": "CA Water Rights Permit #12345" + }, + "pump-locations": { + "pump-in": { + "office-id": "SPK", + "name": "Sac River-Pump 1" + }, + "pump-out": { + "office-id": "SPK", + "name": "Sac River-Pump 2" + }, + "pump-below": { + "office-id": "SPK", + "name": "Sac River-Pump 3" + } + }, + "pump-accounting": { + "2022-11-20T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Temporary Inlet", + "flow": 1.0, + "flow-unit": "cms", + "comment": "Added water to the system" + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 2.0, + "flow-unit": "cms", + "comment": "Removed excess water" + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 3.0, + "flow-unit": "cms", + "comment": "Daily water release" + } + ], + "2023-11-21T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Pipeline", + "flow": 4.0, + "flow-unit": "cms", + "comment": "Pump transfer for the day" + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 5.0, + "flow-unit": "cms", + "comment": "Excess water transfer" + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 6.0, + "flow-unit": "cms", + "comment": "Water returned to the river" + } + ], + "2024-11-22T21:17:28Z": [ + { + "pump-type": "IN", + "transfer-type-display": "Pipeline", + "flow": 7.0, + "flow-unit": "cms", + "comment": "Pump transfer for the day" + }, + { + "pump-type": "OUT", + "transfer-type-display": "Pipeline", + "flow": 8.0, + "flow-unit": "cms", + "comment": "Excess water transfer" + }, + { + "pump-type": "BELOW", + "transfer-type-display": "Pipeline", + "flow": 9.0, + "flow-unit": "cms", + "comment": "Water returned to the river" + } + + ] + } +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml new file mode 100644 index 0000000000..4b9cf1cabd --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/api/vertical_datum_example_rating.xml @@ -0,0 +1,83 @@ + + + + Elev;Area + Standard + + + Elev + LINEAR + NEAREST + NEAREST + + + Area + + + + {location}.Elev;Area.Standard.Production + Elev;Area.Standard + {location} + Production + + LINEAR + NEAREST + NEAREST + true + true + true + true + + 2222233332 + + 2222233332 + + + + {location}.Elev;Area.Standard.Production + + NGVD-29 + 36.089 + + NGVD-29 + 0.0 + + + NAVD-88 + -2.532 + + + ft;acre + 2016-09-06T20:08:00Z + + 2016-09-06T20:08:00Z + true + + + + 620.0 + 0.0 + + + 621.0 + 1.0 + + + 622.0 + 2.0 + + + 623.0 + 3.0 + + + 624.0 + 4.0 + + + 625.0 + 5.0 + + + + diff --git a/cwms-data-api/src/test/resources/cwms/cda/api/waterusercontract.json b/cwms-data-api/src/test/resources/cwms/cda/api/waterusercontract.json index 401c8bb56c..5b8cee7d2d 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/api/waterusercontract.json +++ b/cwms-data-api/src/test/resources/cwms/cda/api/waterusercontract.json @@ -10,7 +10,7 @@ }, "contract-id": { "office-id": "SPK", - "name": "Sac River" + "name": "Sac River Pumps" }, "contract-type": { "office-id": "SPK", diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/parameter_legacy.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/parameter_legacy.json new file mode 100644 index 0000000000..567bb9c43c --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/parameter_legacy.json @@ -0,0 +1,9 @@ +{ + "abstract-param": "Area", + "name": "Area", + "office": "All", + "default-english-unit": "ft2", + "default-si-unit": "m2", + "long-name": "Surface Area", + "description": "Area of a surface" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json new file mode 100644 index 0000000000..d887a1dc91 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.json @@ -0,0 +1,4 @@ +{ + "position": 1, + "value": "12345" +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml new file mode 100644 index 0000000000..b86b36fb24 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/independent_rounding_spec.xml @@ -0,0 +1 @@ +12345 diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json new file mode 100644 index 0000000000..7e8d58bb4a --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.json @@ -0,0 +1,18 @@ +{ + "office-id" : "SWT", + "rating-id" : "ARBU.Elev;Stor.Linear.Production", + "template-id" : "Elev;Stor.Linear", + "location-id" : "ARBU", + "version" : "Production", + "in-range-method" : "NEAREST", + "out-range-low-method" : "LINEAR", + "out-range-high-method" : "LOWER", + "active" : true, + "auto-update" : false, + "auto-activate" : false, + "auto-migrate-extension" : false, + "independent-rounding-specs" : [ { + "value" : "2222233332" + } ], + "dependent-rounding-spec" : "2222233332" +} \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml new file mode 100644 index 0000000000..4da7cc4744 --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_spec.xml @@ -0,0 +1,18 @@ + + SWT + ARBU.Elev;Stor.Linear.Production + Elev;Stor.Linear + ARBU + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json new file mode 100644 index 0000000000..9dc9f5a2ce --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.json @@ -0,0 +1,47 @@ +{ + "page": "MHx8MTB8fDI=", + "page-size": 10, + "total": 2, + "specs": [ + { + "office-id": "SWT", + "rating-id": "ARBU.Elev;Stor.Linear.Production", + "template-id": "Elev;Stor.Linear", + "location-id": "ARBU", + "version": "Production", + "in-range-method": "NEAREST", + "out-range-low-method": "LINEAR", + "out-range-high-method": "LOWER", + "active": true, + "auto-update": false, + "auto-activate": false, + "auto-migrate-extension": false, + "independent-rounding-specs": [ + { + "value": "2222233332" + } + ], + "dependent-rounding-spec": "2222233332" + }, + { + "office-id": "SWT", + "rating-id": "OBRK.Elev;Stor.Linear.Production", + "template-id": "Elev;Stor.Linear", + "location-id": "OBRK", + "version": "Production", + "in-range-method": "NEAREST", + "out-range-low-method": "LINEAR", + "out-range-high-method": "LOWER", + "active": true, + "auto-update": false, + "auto-activate": false, + "auto-migrate-extension": false, + "independent-rounding-specs": [ + { + "value": "2222233332" + } + ], + "dependent-rounding-spec": "2222233332" + } + ] +} diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml new file mode 100644 index 0000000000..1cffcf81bc --- /dev/null +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/rating/rating_specs.xml @@ -0,0 +1,43 @@ + + MCwwLDI= + 10 + 2 + + + SWT + ARBU.Elev;Stor.Linear.Production + Elev;Stor.Linear + ARBU + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + + + SWT + OBRK.Elev;Stor.Linear.Production + Elev;Stor.Linear + OBRK + Production + NEAREST + LINEAR + LOWER + true + false + false + false + 2222233332 + + 2222233332 + + + + diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/pump_accounting.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/pump_accounting.json index 94f0751a40..65952a1d13 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/pump_accounting.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/pump_accounting.json @@ -39,6 +39,7 @@ "active": true }, "flow": 1.0, + "flow-unit": "cms", "transfer-date": 10000012648112, "comment": "Test Comment" } \ No newline at end of file diff --git a/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/water_pump_accounting.json b/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/water_pump_accounting.json index ed63af3ba2..345be6ffc5 100644 --- a/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/water_pump_accounting.json +++ b/cwms-data-api/src/test/resources/cwms/cda/data/dto/watersupply/water_pump_accounting.json @@ -28,18 +28,21 @@ "pump-type": "IN", "transfer-type-display": "Pipeline", "flow": 1.0, + "flow-unit": "cms", "comment": "Added water to the system" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 2.0, + "flow-unit": "cms", "comment": "Removed excess water" }, { "pump-type": "BELOW", "transfer-type-display": "River", "flow": 3.0, + "flow-unit": "cms", "comment": "Daily water release" } ], @@ -48,18 +51,21 @@ "pump-type": "IN", "transfer-type-display": "Pipeline", "flow": 4.0, + "flow-unit": "cms", "comment": "Pump transfer for the day" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 5.0, + "flow-unit": "cms", "comment": "Excess water transfer" }, { "pump-type": "BELOW", "transfer-type-display": "River", "flow": 6.0, + "flow-unit": "cms", "comment": "Water returned to the river" } ], @@ -68,18 +74,21 @@ "pump-type": "IN", "transfer-type-display": "Pipeline", "flow": 7.0, + "flow-unit": "cms", "comment": "Pump transfer for the day" }, { "pump-type": "OUT", "transfer-type-display": "Pipeline", "flow": 8.0, + "flow-unit": "cms", "comment": "Excess water transfer" }, { "pump-type": "BELOW", "transfer-type-display": "River", "flow": 9.0, + "flow-unit": "cms", "comment": "Water returned to the river" } ] diff --git a/cwms-data-api/src/test/resources/ratings_db.txt b/cwms-data-api/src/test/resources/ratings_db.txt index ea69c920da..b690a86094 100644 --- a/cwms-data-api/src/test/resources/ratings_db.txt +++ b/cwms-data-api/src/test/resources/ratings_db.txt @@ -60,7 +60,7 @@ select "CWMS_20"."AV_OFFICE"."OFFICE_ID", "CWMS_20"."AV_CLOB".* from "CWMS_20"." > ----------------------------------------------------------------------------------------------------------------------------------------------------------------- > { "fields": [ {"name": "location", "type": "object"} ], "records": [ [ [{ "base_location_id": "Alder Springs", "sub_location_id": null, "office_id": "SPK" }, "CA", "Glenn", "PST8PDT", null, 0, 0, "WSG84", 30.48, "m", "NAVD88", "Alder Springs", "Alder Springs", "Climate Gage", "T", "SITE", null, null, null, null, null, "UNITED STATES", "Persque Isle"] ] ]} @ rows: 1 -select "CWMS_20"."AV_LOC".* from "CWMS_20"."AV_LOC" where (lower("CWMS_20"."AV_LOC"."DB_OFFICE_ID") = lower(?) and lower("CWMS_20"."AV_LOC"."UNIT_SYSTEM") = lower(?) and lower("CWMS_20"."AV_LOC"."LOCATION_ID") = lower(?)); +select "CWMS_20"."AV_LOC".* from "CWMS_20"."AV_LOC" where ("CWMS_20"."AV_LOC"."DB_OFFICE_ID" = ? and lower("CWMS_20"."AV_LOC"."UNIT_SYSTEM") = lower(?) and lower("CWMS_20"."AV_LOC"."LOCATION_ID") = lower(?)); > LOCATION_CODE BASE_LOCATION_CODE DB_OFFICE_ID BASE_LOCATION_ID SUB_LOCATION_ID LOCATION_ID LOCATION_TYPE UN ELEVATION UNIT_ID VERTICAL_DATUM LONGITUDE LATITUDE HORIZONTAL_DATUM TIME_ZONE_NAME COUNTY_NAME ST PUBLIC_NAME LONG_NAME DESCRIPTION B L LOCATION_KIND_ID MAP_LABEL PUBLISHED_LATITUDE PUBLISHED_LONGITUDE BOUNDING_OFFICE_ NATION_ID NEAREST_CITY A > ------------- ------------------ ---------------- ------------------------ -------------------------------- --------------------------------------------------------- -------------------------------- -- ---------- -------- ---------------- ---------- ---------- ---------------- ---------------------------- ---------------------------------------- -- --------------------------------------------------------- -------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - -------------------------------- -------------------------------------------------- ------------------ ------------------- ---------------- ------------------------------------------------ -------------------------------------------------- - > 0 0 SPK Alder Springs {null} Alder Springs {null} EN 1.0E+002 ft NAVD88 0 0 WGS84 PST8PDT Glenn CA Alder Springs Alder Springs climate gage T T SITE {null} {null} {null} {null} UNITED STATES Presque Isle T diff --git a/cwms-data-api/src/test/resources/tomcat/conf/context.xml b/cwms-data-api/src/test/resources/tomcat/conf/context.xml index 719405a017..a6c898bb72 100644 --- a/cwms-data-api/src/test/resources/tomcat/conf/context.xml +++ b/cwms-data-api/src/test/resources/tomcat/conf/context.xml @@ -16,7 +16,7 @@ testWhileIdle="true" timeBetweenEvictionRunsMillis="5000" logValidationErrors="true" - suspectTimeout="15" + suspectTimeout="8" jdbcInterceptors="QueryTimeoutInterceptor(queryTimeout=600);StatementFinalizer(trace=true);StatementCache(callable=true);SlowQueryReport(threshold=5000);ResetAbandonedTimer" logAbandoned="true" maxAge="3600000" diff --git a/docker-compose.yml b/docker-compose.yml index 8694b4679f..af7c82e191 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,11 @@ volumes: oracle_data_1: auth_data: + minio_data: + driver: local + minio_config: + driver: local + services: db: image: ghcr.io/hydrologicengineeringcenter/cwms-database/cwms/database-ready-ora-23.5:latest-dev @@ -14,7 +19,7 @@ services: - ORACLE_PASSWORD=badSYSpassword - CWMS_PASSWORD=simplecwmspasswD1 - OFFICE_ID=HQ - - OFFICE_EROC=s0 + - OFFICE_EROC=q0 ports: - "1525:1521" healthcheck: @@ -33,11 +38,12 @@ services: - SYS_PASSWORD=badSYSpassword # set to HQ/q0 for any national system work - OFFICE_ID=HQ - - OFFICE_EROC=s0 + - OFFICE_EROC=q0 - INSTALLONCE=1 - QUIET=1 + - API_KEY=testkey12345677 command: > - sh -xc "sqlplus CWMS_20/$$CWMS_PASSWORD@$$DB_HOST_PORT$$DB_NAME @/setup_sql/users $$OFFICE_EROC" + sh -xc "sqlplus CWMS_20/$$CWMS_PASSWORD@$$DB_HOST_PORT$$DB_NAME @/setup_sql/users $$OFFICE_EROC $$API_KEY" volumes: - ./compose_files/sql:/setup_sql:ro depends_on: @@ -55,6 +61,10 @@ services: condition: service_completed_successfully traefik: condition: service_healthy + minio: + condition: service_healthy + minio-setup: + condition: service_completed_successfully image: cwms-rest-api:local-dev build: target: api @@ -73,7 +83,7 @@ services: - JAVA_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -XX:MaxRAMPercentage=75 -XX:MinRAMPercentage=75 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9010 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false - CDA_JDBC_DRIVER=oracle.jdbc.driver.OracleDriver - CDA_JDBC_URL=jdbc:oracle:thin:@db/FREEPDB1 - - CDA_JDBC_USERNAME=s0webtest + - CDA_JDBC_USERNAME=q0webtest - CDA_JDBC_PASSWORD=simplecwmspasswD1 - CDA_POOL_INIT_SIZE=5 - CDA_POOL_MAX_ACTIVE=10 @@ -86,6 +96,14 @@ services: - cwms.dataapi.access.openid.altAuthUrl=http://localhost:${APP_PORT:-8081} - cwms.dataapi.access.openid.useAltWellKnown=true - cwms.dataapi.access.openid.issuer=http://localhost:${APP_PORT:-8081}/auth/realms/cwms + - cwms.dataapi.access.openid.clientId=cwms + # values are not actually used in the local keycloak, however it does fail and leaves them in place for various testing. + - cwms.dataapi.access.openid.idpHint=federation-eams,login.gov + - blob.store.endpoint=http://minio:9000 + - blob.store.region=docker + - blob.store.bucket=cwms-test + - blob.store.accessKey=cda_user + - blob.store.secretKey=cda_password expose: - 7000 - 5005 @@ -106,15 +124,15 @@ services: - "traefik.http.services.data-api.loadbalancer.server.port=7000" auth: - image: quay.io/keycloak/keycloak:19.0.1 + image: quay.io/keycloak/keycloak:26.5 deploy: resources: limits: cpus: .5 memory: 1024m - command: ["start-dev", "--features-disabled=admin2","--import-realm"] + command: ["start-dev", "--import-realm"] healthcheck: - test: "/usr/bin/curl -If localhost:${APP_PORT:-8081}/auth/health/ready || exit 1" + test: "/healthcheck.sh" interval: 5s timeout: 1s retries: 100 @@ -132,6 +150,7 @@ services: - KC_HTTP_RELATIVE_PATH=/auth volumes: - ./compose_files/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro + - ./compose_files/keycloak/healthcheck.sh:/healthcheck.sh:ro labels: - "traefik.enable=true" - "traefik.http.routers.auth.rule=PathPrefix(`/auth`)" @@ -156,7 +175,7 @@ services: expose: - "8080" volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" + - /var/run/docker.sock:/var/run/docker.sock:ro healthcheck: test: traefik healthcheck --ping command: @@ -165,8 +184,52 @@ services: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" - "--ping" - - "--log.level=DEBUG" + - "--log.level=WARN" labels: - "traefik.enable=true" - "traefik.http.routers.traefik.rule=PathPrefix(`/traefik`)" - "traefik.http.routers.traefik.service=api@internal" + + minio: + container_name: minio_server + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + volumes: + - minio_data:/data + - minio_config:/root/.minio + ports: + - '${FORWARD_MINIO_API_PORT0:-9000}:9000' + - '${FORWARD_MINIO_PORT:-9001}:9001' + - '${FORWARD_MINIO_API_PORT2:-9002}:9002' + environment: + MINIO_ROOT_USER: minio_admin + MINIO_ROOT_PASSWORD: saersdbewadfqewrbwreq12rfgweqrffw52354ec@%fwewEFFWSE + command: server /data --console-address ":9001" + healthcheck: + test: [ "CMD", "mc", "ready", "local" ] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + deploy: + resources: + reservations: + cpus: "0.5" + memory: 1G + limits: + cpus: "1.0" + memory: 2G + + minio-setup: + image: minio/mc:latest + restart: "no" + depends_on: + minio: + condition: service_healthy + entrypoint: > + /bin/sh -c " + /usr/bin/mc alias set myminio http://minio:9000 minio_admin saersdbewadfqewrbwreq12rfgweqrffw52354ec@%fwewEFFWSE; + /usr/bin/mc admin user add myminio cda_user cda_password; + /usr/bin/mc mb --ignore-existing myminio/cwms-test; + /usr/bin/mc admin policy attach myminio readwrite --user cda_user; + exit 0; + " diff --git a/docs/authorization-report/AcronymGuide.md b/docs/authorization-report/AcronymGuide.md new file mode 100644 index 0000000000..75a0ad896d --- /dev/null +++ b/docs/authorization-report/AcronymGuide.md @@ -0,0 +1,47 @@ + +# Authorization Report Acronym Guide + +Acronym | Meaning | Context +-- | -- | -- +AAA | HEC AAA Client (authentication/authorization client) | Java client used to interface with CWMS authentication; see https://mvnrepository.com/artifact/mil.army.usace.hec/cwms-aaa-client +ABAC | Attribute-Based Access Control | a paradigm implemented in Database systems to finely control access to tables and data via the attributes of the data. +AIS | Army Information System | general term for any Army IT solution +ANSI | American National Standards Institute | A non-profit organization that develops standards used in IT and other products world-wide. +API | Application Program Interface | An interface at an online resource for other systems to connect to in order to share data +AWS | Amazon Web Services | Amazon's cloud-based computing solution +CAC | Common Access Card | Smart cards used by the DoD and other agencies to authorize persons into IT hardware and software solutions. +CDA | CWMS Data API | API façade (Javalin) used by clients/services to access CWMS DB; also the name of the main GitHub repo +CI/CD | Continuous Integration / Continuous Delivery (or Deployment) | DevSecOps pipeline practice used for automated build, test, and release +COTS | Commercial Off-The Shelf | a term for commercially developed software available for purchase to solve a need, as opposed to a home-grown or internally developed solution +CRUD | Create, Read, Update, and Delete | The four main operations that can be performed on a row of data within a database, and used to define access permissions. +CWMS | Corps Water Management System | Automated information system used to manage USACE's mission. See https://www.hec.usace.army.mil/cwms/ +DBA | Database Administrators | Database management professionals who have super-user access a database and whose job it is to maintain all aspects of the database system and the data contained therein. +DoD | Department of Defense | The Department of Defense, underneath which the Department of the Army sits in the Executive Branch of the government. +ELK | Elasticsearch, Logstash, Kibana | Common logging/observability stack used for metrics and trace analysis +HEC | Hydrologic Engineering Center | HEC provides support to USACE's overall water management mission. See https://www.hec.usace.army.mil/about/MissionStatement.aspx +HMS | Hydrologic Modeling System | An HEC product for hydrologic modeling; see https://www.hec.usace.army.mil/software/hec-hms/ +HTTP | Hypertext Transfer Protocol | the TCP/IP stack protocol that is the basis for the modern internet, enabling hyper linking between web sites. See https://en.wikipedia.org/wiki/HTTP +IT | Information Technology | a general term for systems and solutions built in and around computing platforms. +IWR | Institute for Water Resources | Overall organization supervising the work of CWMS +JSON | JavaScript Object Notation | an Open source file format/data interchange format that has become a defacto standard for sending data in the modern era. See https://en.wikipedia.org/wiki/JSON +JWT | JSON Web Token | A method for providing authentication and encryption in JSON packages. See https://en.wikipedia.org/wiki/JSON_Web_Token +NOAA | National Oceanic and Atmospheric Administration | External agency that utilizes this dataset. See https://www.noaa.gov/ +OIDC | OpenID Connect | An open source authentication protocol that enables users to be authenticated by 3rd party sites. See https://en.wikipedia.org/wiki/OpenID +OPA | Open Policy Agent | An open-source policy engine that streamlines policy implementations for organizations. see https://www.openpolicyagent.org/ +PWS | Performance Work Statement | Government terminology for the document that defines a solution to which the government seeks a contracting proposal from private companies to pay to solve. +QA | Quality Assurance | Within the context of a data-centric system, Quality Assurance are analysis activities performed on data to ensure the cleanliness and accurateness of data before adding it to the larger dataset. +Q-38 | RFC Question #38 | Q-38 was an important question raised during the pre-award solicitation Q&A process where USACE staff answered the question, ' What are the specific limitations of the current Role-Based Access Control that the new authorization model needs to address?" The government's response helped define the solutioning found in this report. +PL/SQL | Procedural Language/Structured Query Language | This is Oracle's proprietary implementation of ANSI-standard SQL, which extends the standard to perform operations that are only supported in Oracle RDBMS solutions. +RAS | River Analysis System | An HEC-developed flow-analysis system. See https://www.hec.usace.army.mil/software/hec-ras/ +RBAC | Role-Based Access Control | a paradigm implemented in Database systems to control access to tables and data via the use of roles and users assigned to those roles +RDS | Relational Database Service | Amazon Web Service's managed database subsystem +REST | Representational State Transfer | An architectural guideline used to develop internet-based solutions. See https://en.wikipedia.org/wiki/REST +RDBMS | Relational Database Management System | A system used to store data in a relational manner. These are 3rd party/COTS enterprise products usually well known within the IT industry. +RLS | Row-Level Security | PostgreSQL feature for row-based access control; target future-state equivalent of Oracle VPD +SCADA | Supervisory Control and Data Acquisition | A computerized system that monitors and controls industrial processes. In the case of HEC, these solutions typically refer to Automated Collection Systems such as water level monitors and other automated systems that feed data into the database. +SQL | Structured Query Language | An ANSI-defined standard methodology for working with data stored within an RDBMS platform. +TLS | Transport Layer Security | Cryptographic protocol securing API/OPA traffic (HTTPS) +UI | User Interface | The method by which users interact with a computer system. +USGS | United States Geological Survey | External federal agency that partners with USACE to provide data. See https://www.usgs.gov/ +VPD | Virtual Private Database | The Oracle feature that controls the security in the current solution. +USACE | United States Army Corps of Engineers | Charged with providing Engineering solutions to the Army. see https://www.usace.army.mil/ diff --git a/docs/authorization-report/Api-Overview.rst b/docs/authorization-report/Api-Overview.rst new file mode 100644 index 0000000000..00ad3eeae6 --- /dev/null +++ b/docs/authorization-report/Api-Overview.rst @@ -0,0 +1,112 @@ +TimeSeries API Discussion Table (with Examples & Issues) +========================================================= + +.. list-table:: + :header-rows: 1 + :widths: 20 25 25 30 + + * - Endpoint (Swagger Link) + - Highlights from Discussion + - Tests / Recommendations & Issues + - Example Calls + * - `GET /timeseries `__ + - - Core endpoint, most used in CDA. + - **Office is required** even though docs once showed optional. + - Defaults: 24h window, 500 records per page. + - Supports ``unit-system`` (EN/SI), ``datum``, ``version-date``. + - Supports ``include-entry-date``. + - ``trim=true`` removes leading/trailing nulls. + - ``regular`` vs ``pseudo-irregular`` series behavior explained. + - - Always specify ``office``. + - Use ``America/Chicago`` instead of ``US/Central``. + - **Time Zone Issue**: Defaults to UTC if not specified. + - **Trim + Page-size Edge Case**: Some values disappeared unless page-size adjusted. + - Raise ``page-size`` or parallelize by date slices for large pulls. + - For CSV downloads in browsers: prefer ``format=csv`` query (**Format vs Accept Header Conflict**). + - **curl**:: + + curl "https://cwms-data.usace.army.mil/cwms-data/timeseries?name=KEYS.ELEV.Inst.1Hour.0.CCP-RAW&office=SWT&begin=2025-08-01T00:00:00Z&end=2025-08-07T00:00:00Z&unit-system=EN&timezone=America/Chicago&page-size=2000&format=csv" -o data.csv + + **Python (requests)**:: + + import requests + r = requests.get("https://cwms-data.usace.army.mil/cwms-data/timeseries", + params={ + "name": "KEYS.ELEV.Inst.1Hour.0.CCP-RAW", + "office": "SWT", + "begin": "2025-08-01T00:00:00Z", + "end": "2025-08-07T00:00:00Z", + "unit-system": "EN", + "timezone": "America/Chicago", + "page-size": 2000 + }, + headers={"Accept": "application/json"}) + print(r.json()) + * - `GET /timeseries/recent `__ + - - Used for dashboards, gets most recent values. + - Accepts ``ts-ids`` (comma separated), ``group-id``, or ``category-id``. + - Performance fixes (latest ~300ms). + - - Use comma-separated ``ts-ids``. + - **Group/Category Issue**: returned empty in tests → likely bug. + - Watch URI length (~2000 chars); chunk large sets. + - **curl**:: + + curl "https://cwms-data.usace.army.mil/cwms-data/timeseries/recent?office=SWT&ts-ids=KEYS.ELEV.Inst.1Hour.0.CCP-RAW,KEYS.FLOW.Inst.1Hour.0.CCP-RAW" + + **Python**:: + + r = requests.get("https://cwms-data.usace.army.mil/cwms-data/timeseries/recent", + params={ + "office": "SWT", + "ts-ids": "KEYS.ELEV.Inst.1Hour.0.CCP-RAW,KEYS.FLOW.Inst.1Hour.0.CCP-RAW" + }) + print(r.json()) + * - `GET /timeseries/filtered `__ + - - SQL-like queries (``greater than``, ``less than``, etc.). + - Session noted powerful but time-consuming. + - - Use only when filtering by values is essential. + - Test carefully on large ranges; may impact performance. + - **curl**:: + + curl "https://cwms-data.usace.army.mil/cwms-data/timeseries/filtered?office=SWT&name=KEYS.ELEV.Inst.1Hour.0.CCP-RAW&where=value>500" + + **Python**:: + + r = requests.get("https://cwms-data.usace.army.mil/cwms-data/timeseries/filtered", + params={ + "office": "SWT", + "name": "KEYS.ELEV.Inst.1Hour.0.CCP-RAW", + "where": "value>500" + }) + print(r.json()) + * - `GET /timeseries/profiles `__ + - - 2D profiles (e.g., values by depth). + - Session confirmed little/no data on national instance. + - - Verify your district has data before building UI. + - May be optional feature. + - **curl**:: + + curl "https://cwms-data.usace.army.mil/cwms-data/timeseries/profiles?office=SWT&name=PROFILE.DEPTH.1Day.0.WQ" + + **Python**:: + + r = requests.get("https://cwms-data.usace.army.mil/cwms-data/timeseries/profiles", + params={ + "office": "SWT", + "name": "PROFILE.DEPTH.1Day.0.WQ" + }) + print(r.json()) + * - `GET /catalog/timeseries `__ + - - Lists available time series, units, intervals, time zones. + - Essential for validation. + - - Query before building dashboards to ensure valid IDs. + - Use to distinguish ``regular`` vs ``pseudo-irregular`` (tilde in ID). + - **curl**:: + + curl "https://cwms-data.usace.army.mil/cwms-data/catalog/timeseries?office=SWT" + + **Python**:: + + r = requests.get("https://cwms-data.usace.army.mil/cwms-data/catalog/timeseries", + params={"office": "SWT"}) + print(r.json()) diff --git a/docs/authorization-report/PWS.pdf b/docs/authorization-report/PWS.pdf new file mode 100644 index 0000000000..5372a25f4e Binary files /dev/null and b/docs/authorization-report/PWS.pdf differ diff --git a/docs/authorization-report/README.md b/docs/authorization-report/README.md new file mode 100644 index 0000000000..1b28876151 --- /dev/null +++ b/docs/authorization-report/README.md @@ -0,0 +1,36 @@ + +# USACE CWMS Authorization Methods Report + +This report provides a comprehensive analysis and set of recommendations for modernizing the authorization methods used in the U.S. Army Corps of Engineers (USACE) Corps Water Management System (CWMS). The report is structured as a multi-section document, with each file representing a distinct chapter or analysis area. + +The primary goal is to evaluate the current Oracle Virtual Private Database (VPD) approach, document its limitations, and propose a future-ready authorization architecture that meets the requirements outlined in the Performance Work Statement (PWS) and stakeholder interviews. The report covers: + +- An inventory and analysis of the current CWMS Data API and its security model +- Detailed use cases for all user personas, including operational staff, automated systems, and external partners +- A gap analysis of CRUD permissions and policy requirements for each persona +- Comparative evaluation of candidate policy models, including OPA-based and traditional RBAC/ABAC approaches +- Security and performance analysis aligned with NIST RMF standards +- A final comparative analysis and recommendation for adopting a policy-as-code approach using Open Policy Agent (OPA) + - An implementation and UI plan outlining delivery options and admin tooling + - An acronym guide centralizing terminology used across sections + +The report is intended for technical stakeholders, system architects, and decision-makers responsible for CWMS security and modernization. It provides both high-level strategy and detailed technical guidance to support a secure, flexible, and future-proof authorization framework for CWMS. + +--- + + +## Table of Contents + +- [Current Oracle VPD Architecture & Analysis](./RptSec1-VPD.md) +- [Inventory & Analysis of CWMS-Data-API Code](./RptSec2-API.md) +- [Gather Use-Cases & Dependencies](./RptSec3-UseCases.md) +- [CRUD-Permission Gap Analysis](./RptSec4-CRUDGapAnalysis.md) +- [Candidate Policy Model Alternatives](./RptSec5-PolicyCandidates.md) +- [NIST RMF-Aligned Security & Performance Analysis](./RptSec6-NIST.md) +- [Comparative Analysis & Recommendation](./RptSec7-ComparativeAnalysis.md) +- [Implementation & UI Plan](./RptSec8-ImplementationAndUI.md) +- [Acronym Guide](./AcronymGuide.md) + +--- + +Each file in this repository represents a separate section of the report. Open the relevant file to view the corresponding content. \ No newline at end of file diff --git a/docs/authorization-report/RptSec1-VPD.md b/docs/authorization-report/RptSec1-VPD.md new file mode 100644 index 0000000000..44133e63b8 --- /dev/null +++ b/docs/authorization-report/RptSec1-VPD.md @@ -0,0 +1,275 @@ + +# Current Oracle VPD Architecture & Analysis + +## Executive Summary + +Oracle Virtual Private Database (VPD) has served as the core authorization mechanism for CWMS, providing row-level security and office-based data isolation. While effective for basic access control, VPD cannot meet the modern, persona-driven, and policy-based requirements outlined in the PWS. This section analyzes the current VPD implementation, its strengths and limitations, and outlines a migration path to a more flexible, auditable, and future-proof authorization architecture using Open Policy Agent (OPA). + +Note on Q‑38: Throughout this report we reference “Q‑38,” the solicitation Q&A item that documents the limitations of the current office‑centric RBAC approach. In short: “You can modify data for a given office” and “You can administer users in a given office,” with sensitive third‑party data withheld from public systems to avoid embargo issues. Q‑38 defines today’s baseline (Modifier/Admin at the office scope) and motivates the persona‑ and policy‑based model. A deeper analysis appears in [Section 4: CRUD‑Permission Gap Analysis](./RptSec4-CRUDGapAnalysis.md#q-38-role-model-and-its-limitations), including the [verbatim source](./RptSec4-CRUDGapAnalysis.md#verbatim-source-from-solicitation-qa). + +## The Q-38 Model: Background + +“Q‑38” refers to Question 38 from the solicitation’s Q&A, which describes the current RBAC baseline and its constraints. In practical terms: + +- Modify data only within your assigned office (district) +- Administer users only within your assigned office +- To avoid embargo conflicts, sensitive third‑party data is not published to public national systems + +Why this matters: these office‑centric constraints cannot express the persona‑specific, time‑based, and parameter‑level rules required by PWS Exhibit 3 and interview‑refined roles, motivating a persona‑driven, policy‑as‑code approach (OPA) elaborated in [Section 3](./RptSec3-UseCases.md) and [Section 5](./RptSec5-PolicyCandidates.md). + +This baseline is summarized here and analyzed in detail in [Section 4](./RptSec4-CRUDGapAnalysis.md#q-38-role-model-and-its-limitations). + +The Q‑38 model refers to the legacy CWMS role-based access control scheme, which defines only two privileges at the office (district) level: + +| Role | Description | +|-----------------|-----------------------------------------------------------------------------| +| **Modifier** | Can create, update, and delete data for their assigned office. | +| **Administrator** | Includes modifier rights, and can also manage user roles in their office. | + +This model is simple and office-centric, but lacks support for fine-grained personas, time-based rules, and modern audit requirements. See the CRUD Gap Analysis section for a detailed comparison. + +### Security and Audit Gaps in Current VPD + +- **No justification logging**: Modifications are not accompanied by user-provided reasons or justifications. +- **Limited auditability**: VPD enforces access but does not provide a detailed audit trail of authorization decisions or policy evaluations. +- **No support for two-person approval**: Cannot enforce workflows requiring multiple approvals for sensitive actions. +- **No parameter-level filtering**: Cannot restrict access to specific parameters or data fields. +- **No time-based or contextual rules**: Lacks support for embargoes, shift windows, or emergency overrides. + +## Glossary + +- **VPD**: Virtual Private Database (Oracle feature for row-level security) +- **OPA**: Open Policy Agent (policy-as-code engine) +- **CRUD**: Create, Read, Update, Delete (basic data operations) +- **PWS**: Performance Work Statement (requirements document) +- **Persona**: A user type or role with specific access needs +- **Q-38**: Legacy CWMS role model with office-centric permissions + + +## Current VPD Implementation + +The CWMS database uses Oracle's Virtual Private Database (VPD) technology to enforce row-level security through database policies. The implementation consists of security tables, stored procedures, and database policies that automatically filter data based on user context. + +### Security Schema Structure + +#### Core Security Tables + +```mermaid +graph TB + subgraph "User Management" + USERS[at_sec_cwms_users
Base user registry] + LOCKED[at_sec_locked_users
Account lockout status] + end + + subgraph "Role-Based Access" + UGROUPS[at_sec_user_groups
User role definitions] + CUSERS[at_sec_users
User-to-group assignments] + PRIVS[cwms_sec_privileges
Permission definitions] + end + + subgraph "Data Access Control" + TSGROUPS[at_sec_ts_groups
Timeseries groupings] + TSMASKS[at_sec_ts_group_masks
Pattern-based access] + ALLOW[at_sec_allow
Permission matrix] + end + + USERS --> CUSERS + UGROUPS --> CUSERS + TSGROUPS --> TSMASKS + TSGROUPS --> ALLOW + UGROUPS --> ALLOW + PRIVS --> ALLOW +``` + +#### Current User Groups (Q-38 Compatible) + +| Group Code | Group ID | Description | Current Usage | +| ---------- | ---------------- | ------------------------------- | -------------------------- | +| 0 | CWMS DBA Users | Super users with all privileges | System administration | +| 1 | CWMS PD Users | Full database write access | Data collection/management | +| 7 | CWMS User Admins | User management capabilities | Account administration | +| 10 | All Users | General CWMS users | Basic access | +| 11 | CWMS Users | Routine users | Standard operations | +| 12 | Viewer Users | Limited access users | Read-only access | + +### VPD Session Context Management + +The current implementation uses the `CWMS_ENV` package to set database session context: + +#### Session Context Setup (AuthDao.java:60-65) + +```java +private static final String SET_API_USER_DIRECT = "begin " + + "cwms_env.set_session_user_direct(upper(?));" + + "end;"; + +private static final String SET_API_USER_DIRECT_WITH_OFFICE = "begin " + + "cwms_env.set_session_user_direct(upper(?),upper(?)); end;"; +``` + +#### Data Filtering Process + +1. **Authentication**: User credentials validated via API key or JWT +2. **Session Setup**: `CWMS_ENV.set_session_user_direct()` establishes user context +3. **Automatic Filtering**: VPD policies automatically apply WHERE clauses +4. **Office Isolation**: Data restricted to user's assigned office(s) + +### Current VPD Limitations vs PWS Requirements + +#### What VPD Handles Well + +- **Office-based filtering**: Automatic restriction to assigned offices +- **Basic role enforcement**: Simple read/write permissions +- **User group management**: Hierarchical role assignments + +#### Critical Gaps for PWS Exhibit 3 Personas + +| PWS Persona | VPD Limitation | Required Enhancement | +| -------------------- | --------------------------- | --------------------------------------- | +| **Dam Operator** | No data source restrictions | Cannot enforce MANUAL-only constraint | +| **Water Manager** | No time-based rules | Cannot implement embargo override | +| **Data Manager** | No audit requirements | Cannot enforce justification logging | +| **Auto Collector** | No append-only enforcement | Cannot prevent historical modifications | +| **Auto Processor** | No derived data distinction | Cannot restrict to calculated outputs | +| **External Partner** | No parameter filtering | Cannot whitelist specific parameters | + +### Database Schema Modifications Needed + +These are the database schema modifications we believe would be needed in order to support our proposed solution. We are proposing to add three new tables and make two small DDL alterations to existing tables. + + +#### New Tables for OPA Integration + +```sql +-- User persona assignments (replacing simple user groups) +CREATE TABLE cwms_auth_user_personas ( + user_id VARCHAR2(128), + persona_code VARCHAR2(32), + office_code NUMBER, + effective_date DATE, + expiry_date DATE, + constraints JSON -- Persona-specific constraints +); + +-- Office-specific configuration +CREATE TABLE cwms_auth_office_config ( + office_code NUMBER, + embargo_hours NUMBER DEFAULT 168, -- 7 days + timezone VARCHAR2(32), + manual_entry_window_hours NUMBER DEFAULT 24 +); + +-- Authorization audit trail +CREATE TABLE cwms_auth_decisions ( + decision_id NUMBER, + user_id VARCHAR2(128), + resource_type VARCHAR2(64), + operation VARCHAR2(16), + decision VARCHAR2(16), -- ALLOW/DENY + policy_version VARCHAR2(32), + timestamp TIMESTAMP, + context JSON +); +``` + +#### Existing Table Enhancements + +```sql +-- Add persona tracking to existing user table +ALTER TABLE at_sec_cwms_users ADD ( + primary_persona VARCHAR2(32), + persona_constraints JSON, + last_policy_sync TIMESTAMP +); + +-- Add data source tracking to timeseries +ALTER TABLE at_cwms_ts_data ADD ( + data_source VARCHAR2(16) DEFAULT 'UNKNOWN', + source_system VARCHAR2(64), + entry_method VARCHAR2(16) -- MANUAL, AUTOMATED, CALCULATED +); +``` + +### VPD Migration Strategy + +#### Phase 1: Parallel Operation + +- **Current VPD**: Continues operating for existing functionality +- **OPA Layer**: Added as overlay for enhanced authorization +- **Data Flow**: Authorization Service → OPA decision → VPD context + +#### Phase 2: Gradual Migration + +- **Office-by-office**: Migrate one office at a time +- **Validation**: Compare OPA vs VPD decisions +- **Rollback**: Ability to disable OPA per office + +#### Phase 3: VPD Removal + +Once OPA proves reliable and complete: + +```sql +-- Remove VPD policies +DROP POLICY cwms_ts_data_policy ON at_cwms_ts_data; +DROP POLICY cwms_location_policy ON at_physical_location; + +-- Remove security context procedures +DROP PROCEDURE cwms_env.set_session_user_direct; +DROP PROCEDURE cwms_env.set_session_office_id; + +-- Simplify schema by removing VPD-specific tables +DROP TABLE at_sec_allow; +DROP TABLE at_sec_ts_group_masks; +-- (Keep user/group tables for reference) +``` + +### Integration Points with Authorization Service + +#### Current Java API Integration (AuthDao.java) + +- **Line 179-192**: Session context setup for database connections +- **Line 291-307**: Role retrieval for authorization decisions +- **Line 346-361**: Basic role validation logic + +#### Enhanced Integration with OPA + +The Authorization Service will: + +1. **Intercept requests** before they reach Java API +2. **Make authorization decisions** using OPA policies +3. **Set enhanced context** via `x-cwms-auth-context` header +4. **Maintain VPD compatibility** during transition period + +### Performance Considerations + +#### Current VPD Performance + +- **Query Performance**: VPD adds WHERE clauses to every query +- **Cache Efficiency**: Database-level security context caching +- **Scalability**: Limited by database connection pool size + +#### OPA Performance Benefits + +- **API-Level Filtering**: Decisions made before database queries +- **Intelligent Caching**: Policy decisions cached independently +- **Reduced Database Load**: Fewer complex queries with VPD conditions + +### Migration Phases + +| Phase | Key Activities | +| ----------- | ------------------------------------------------ | +| **Phase 1** | Deploy Authorization Service, parallel operation | +| **Phase 2** | Office-by-office migration, validation | +| **Phase 3** | VPD removal, schema cleanup | + +## Conclusion + +The proposed OPA-based Authorization Service will: + +1. **Maintain compatibility** with existing VPD during migration +2. **Enable complex authorization rules** not possible with database-only policies +3. **Provide better performance** by making decisions at the API layer +4. **Support future migration** to PostgreSQL with minimal changes + +The VPD system can be completely removed once the OPA-based authorization is proven reliable, eliminating the complexity of database-level security policies while providing more flexible and maintainable authorization. + diff --git a/docs/authorization-report/RptSec2-API.md b/docs/authorization-report/RptSec2-API.md new file mode 100644 index 0000000000..6a42162bbf --- /dev/null +++ b/docs/authorization-report/RptSec2-API.md @@ -0,0 +1,124 @@ + +# Section 2: CWMS Data API Inventory and Authorization Architecture Analysis + +This section provides a comprehensive inventory and analysis of the CWMS Data API, with a focus on its current authentication and authorization mechanisms. The purpose of this section is to document the structure, endpoints, and security model of the API, serving as a foundation for identifying gaps and recommending improvements to meet the requirements of the PWS (Performance Work Statement). Readers will gain an understanding of the API’s organization, available resources, and how access is currently controlled. + +Reference: [CWMS Data API Issue #1153](https://github.com/USACE/cwms-data-api/issues/1153) + + +## API Reference + +The full, interactive API documentation for the CWMS Data API is available via Swagger UI: + +[CWMS Data API Swagger UI](https://cwms-data.usace.army.mil/cwms-data/swagger-ui.html) + + +## API Endpoint Inventory + +### Overall Statistics + +- **Total Controller Classes**: 96 +- **Total REST Endpoints**: 279 (all HTTP methods) +- **Logical Resource Groups**: 62 +- **Authentication Methods**: 3 (API Key, CWMS AAA, OpenID Connect) +- **Current Authorization Roles**: 2 primary (CWMS Users, CWMS User Admins) + + +### Endpoint Categories + +The API endpoints are organized into the following categories: + +#### Core Data Entities (23 resource types) +Primary data resources, including: +- **Timeseries** (standard, binary, text, profile) +- **Locations** and location metadata +- **Levels** and specified levels +- **Ratings** and rating templates +- **Projects** and project components +- **Streams** and stream reaches +- **Forecasts** (specs and instances) + +#### Configuration & Metadata (15 resource types) +System configuration resources, such as: +- **Offices** and office settings +- **Units** and unit conversions +- **Parameters** and parameter definitions +- **Categories** and groupings +- **Lookup types** and standard text + +#### Project Sub-entities (12 resource types) +Specialized project components, including: +- **Outlets** and virtual outlets +- **Gate changes** and turbine changes +- **Water users** and contracts +- **Pumps** and accounting + +#### Authentication & Authorization (7 resource types) +Security-related endpoints: +- **API Keys** management +- **Users** and user profiles +- **Roles** (limited implementation) +- **Project locks** and lock rights + + +## Current Authorization Architecture + +### Authentication and Authorization Flow + +```mermaid +flowchart TD + A[Client Request] --> B[IdentityProvider
API Key/CWMS AAA/OIDC] + B --> C[DataApiPrincipal Creation] + C --> D[CdaAccessManager Validation] + D --> E[AuthDao Permission Check] + E --> F[Database Context Setup] +``` + + +### Key Components + +#### 1. CdaAccessManager (`cwms/cda/security/CdaAccessManager.java`) +- Central authorization enforcement point +- Manages rate limiting for protected endpoints +- Prepares database connection context +- Delegates authorization to AuthDao + +#### 2. AuthDao (`cwms/cda/data/dao/AuthDao.java`) +- Validates user permissions against required roles +- Manages API key authentication +- Sets Oracle session context via `cwms_env.set_session_user_direct()` +- Interfaces with Oracle VPD for data filtering + +#### 3. Role Definition (`cwms/cda/security/Role.java`) +- Simple role implementation +- Currently defines only: + - CWMS Users (basic authenticated access) + - CWMS User Admins (user management) + - CAC User (certificate-based auth) + + +### Authorization Touchpoints + +1. **Route Registration** (`ApiServlet.java`) + - Example: + ```java + crud("/auth/keys/{key-name}", controller, new Role[]{CAC_USER, CWMS_USERS_ROLE}) + ``` +2. **Controller Layer** + - Each controller specifies required roles + - CRUD operations inherit base permissions + - No resource-level authorization +3. **Database Layer** + - Oracle VPD enforces office-based filtering + - Session context determines data visibility + - Controls are limited to the database level + + + + +## Analysis & Conclusions + +The CWMS Data API is comprehensive, supporting a wide range of data and configuration operations through numerous endpoints and resource types. However, the current authorization model is simple and coarse-grained, relying on just two main roles and lacking resource-level (fine-grained) access controls. Security enforcement is centralized in the CdaAccessManager and AuthDao, with Oracle VPD providing office-based data filtering at the database level. While this architecture is robust for basic role-based access and office-level data segregation, it offers limited support for advanced authorization scenarios such as attribute-based access control (ABAC), delegated permissions, or dynamic policy enforcement. The API supports multiple authentication methods, but these are not fully integrated into a unified authorization strategy. To meet more complex or evolving security requirements, enhancements are needed to expand the role model, introduce resource-level permissions, and improve the flexibility and auditability of authorization decisions. The current design does provide a clear integration point for enhanced authorization middleware, which will be necessary to support more granular access control and compliance with future policy requirements. + + + diff --git a/docs/authorization-report/RptSec3-UseCases.md b/docs/authorization-report/RptSec3-UseCases.md new file mode 100644 index 0000000000..57dff1d758 --- /dev/null +++ b/docs/authorization-report/RptSec3-UseCases.md @@ -0,0 +1,584 @@ +# Gather Use-Cases & Dependencies + +https://github.com/USACE/cwms-data-api/issues/1135 + +## Table of Contents + +- [Overview](#overview) +- [Interview-Derived Role Gaps](#interview-derived-role-gaps) +- [Implications for Authorization Design](#implications-for-authorization-design) +- [End to End Persona Data Flows](#end-to-end-persona-data-flows) +- [Persona Use Cases](#persona-use-cases) + - [Anonymous/Public](#persona-anonymous-public) + - [Dam Operator](#persona-dam-operator) + - [Water Manager](#persona-water-manager) + - [Data Manager](#persona-data-manager) + - [Automated Collection System](#persona-auto-collection) + - [Automated Processing System](#persona-auto-processing) + - [External Cooperator](#persona-external-cooperator) + - [Facilities Staff](#persona-facilities-staff) + - [Authorization Admin](#persona-authorization-admin) + - [Data Steward (QA)](#persona-data-steward) + - [Diagnostics Engineer](#persona-diagnostics-engineer) + - [Partner Data Controller](#persona-partner-data-controller) + - [Water Quality Manager](#persona-water-quality-manager) +- [System Dependencies](#system-dependencies) +- [Policy Implementation Summary](#policy-implementation-summary) + +## Overview + +This section documents end-to-end use cases for each of the seven user personas identified through stakeholder interviews and PWS requirements. Each use case includes data access scenarios, dependencies on existing systems, and policy requirements for the new authorization framework. + +The CWMS Database Authorization framework relies on a persona-based access control model to define and enforce policy boundaries. The Performance Work Statement (PWS Exhibit 3) identifies seven core personas: **Anonymous/Public**, **Water Manager**, **Data Manager**, **Dam Operator**, **Automated Collection System**, **Automated Processing System**, and **External Cooperator**. These serve as the baseline for both Role-Based Access Control (RBAC) and emerging Attribute-Based Access Control (ABAC) implementations. + +However, based on interviews with CWMS stakeholders, HEC staff, and regional users, it is clear that these personas alone do not fully capture the complexity of operational roles, technical responsibilities, and policy delegation needs. This section documents those gaps and proposes a refined persona model to better align authorization patterns with real-world workflows. + +--- + +Cross‑reference: For the legacy baseline that motivates these personas, see Q‑38 context in [Section 1](./RptSec1-VPD.md#the-q-38-model-background) and the deeper analysis in [Section 4](./RptSec4-CRUDGapAnalysis.md#q-38-role-model-and-its-limitations). + +### Interview-Derived Role Gaps + +Stakeholder interviews conducted by the CWMS team revealed six critical persona refinements or additions. These are not speculative extensions but grounded observations from current system users, administrators, and integrators. + +#### Updated and Refined Personas + +| Persona | Description | Source(s) | +| --- | --- | --- | +| **Facilities Staff** *(Refined from Dam Operator)* | Field personnel who manually input readings or adjustments. Require strict enforcement that limits writes to `seriesType = MANUAL`. | Ruth Koehnke, Brandon Kolze | +| **Authorization Admin** | Responsible for assigning and revoking access roles, either directly or through policy attributes. Needs visibility and control via a dedicated interface. | Charles Graham | +| **Data Steward (QA)** | Handles post-ingest quality assurance by flagging, annotating, and submitting non-destructive corrections. Should not have full overwrite permissions. | Jessica Batterman, Charles Graham | +| **Diagnostics Engineer** | Supports ingest pipelines, data processing, and system health. Requires controlled, read-only access to logs, metrics, and error states. | Ruth Koehnke, synthesized via Todd Boss | +| **Partner Data Controller** | External cooperators with legal, regulatory, or contractual rights to control availability and visibility of their data (e.g., embargoes, legal holds). | Brian Cosgrove | +| **Water Quality Manager** *(Split from Water Manager)* | Manages environmental data (e.g., water chemistry), often with different access needs than operational control staff. May require attribute-based access to quality-specific fields. | Jessica Batterman, synthesized via Todd Boss | + +These role definitions underscore the need for more granular control at the intersection of persona, data type, and operation. In particular, they highlight situations where existing personas are too coarse to enforce safe or auditable interactions. + +--- + +### Implications for Authorization Design + +Each refined persona introduces distinct implications for the authorization model: + +- **Facilities Staff** require UI-level and policy-layer validation to block any write attempts outside designated manual input series. Failure to constrain this creates risk of corrupting operational datasets. + +- **Authorization Admins** must be modeled as a formal system role, with scoped privileges to assign personas, attributes, and policy overrides within their command area. + +- **Data Stewards** must have constrained write paths and full auditability. Any system handling QA operations must differentiate between destructive and non-destructive updates. + +- **Diagnostics Engineers** represent a recurring operational blind spot. Providing safe visibility into ingest status, logs, and pipeline behavior reduces reliance on insecure workarounds like direct SQL access or SSH tunneling. + +- **Partner Data Controllers** introduce attribute-driven constraints (e.g., `releaseDate`, `legalHold`, `dataOwner`) that must be incorporated into ABAC policy logic to respect external ownership and obligations. + +- **Water Quality Managers** reflect intra-agency divergence in data stewardship, with potential need for schema or tag-based access constraints to prevent over-provisioning. + +--- + +### End to End Persona Data Flows +--- +#### Universal Column/Swim‑lane Reference + +| Lane | Description | +| --- | --- | +| **Client / Persona** | Browser, thick client, sensor, batch job, or admin UI | +| **API Gateway** | HTTPS entry point, rate‑limiter, TLS terminator | +| **AuthN** | CAC / OIDC / API‑Key validator that issues a JWT with persona, office, partner, etc. | +| **CDA Service** | CWMS Data API façade (Javalin) that calls Oracle / Postgres procedures | +| **OPA Policy Engine** | Evaluates RBAC + ABAC rules and returns allow / deny | +| **RDBMS** | CWMS database (legacy Oracle VPD today; future Postgres RLS) | +| **Audit & Logs** | CloudWatch / ELK structured logs and policy decision traces | + +--- +#### Interfaces used by Personas + +| Persona (13) | Interface(s) / Tools Used | Notes on Access Method | +|----------------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| [**Anonymous / Public**](#persona-anonymous-public) | Web Browser (Public CWMS UI) | Accesses `/timeseries` endpoint; no authentication required | +| [**Dam Operator**](#persona-dam-operator) | CWMS CAC Thick Client, HEC-DSSVue, or Local SCADA UI | Manual entry through authenticated thick client or form-based UI | +| [**Water Manager**](#persona-water-manager) | CWMS CAC Client, Web Dashboard, Excel Templates (ingested via CDA) | Authenticated access; can override embargoes under `emergency=true` context | +| [**Data Manager**](#persona-data-manager) | CWMS CAC Client, SQL Developer, Custom Bulk QA Tools | Admin-level bulk editing tools with approval workflow integration | +| [**Auto-Collection System**](#persona-auto-collection) | Direct API integration (HTTPS POST), IoT-enabled SCADA/Logger Hardware | API key-authenticated sensor push to data ingest endpoint | +| [**Auto-Processing System**](#persona-auto-processing) | Backend Java Batch Jobs, Python Model Scripts, Jenkins CI/CD | Executes service-to-service batch transformations and writes calculated series | +| [**External Cooperator**](#persona-external-cooperator) | Partner System API Clients (e.g., NOAA’s ETL job, USGS streamflow sync tool) | OIDC token-based access; restricted by partner agreement | +| [**Facilities Staff**](#persona-facilities-staff) | CWMS CAC or Facility-level UI (subset of Dam Operator tools) | Manual input interfaces, with constrained time-of-day and series-type validation | +| [**Authorization Admin**](#persona-authorization-admin) | Admin Web UI or CLI Tooling (e.g., `grant_role` scripts, API calls) | Modifies policy bundles, assigns permissions; triggers rebuild of policy engine state | +| [**Data Steward (QA)**](#persona-data-steward) | Custom QA Flagging UI, CWMS CAC Light Mode | Interface only allows flagging or non-destructive annotations | +| [**Diagnostics Engineer**](#persona-diagnostics-engineer) | Log Dashboards (e.g., Kibana, CloudWatch), API Health Endpoints | Read-only access to observability endpoints and ingestion job logs | +| [**Partner Data Controller**](#persona-partner-data-controller) | Partner Portal, Metadata Admin API | Allows manipulation of release windows, legal holds, embargo attributes | +| [**Water Quality Manager**](#persona-water-quality-manager) | CWMS CAC Client with Chemistry Plugin, Statistical Analysis R/Notebooks | Reads CHEM_* parameters; may write derived stats via CALCULATED series endpoint | +* * * * * +#### End‑to‑End Flow Table (All Personas) +| Persona (13) | Client Action | Gateway / AuthN | CDA Service Step | OPA Policy – Key Gates | Database Ops | Audit / Logging | +|----------------------------|----------------------------------------|---------------------------------------------|-----------------------------------------|--------------------------------------------------------------------------------|------------------------------------------|---------------------------------------------| +| [**Anonymous / Public**](#persona-anonymous-public) | GET /timeseries (no auth) | Tags request `public`; no JWT | Passes context `persona=public` | • Data `public=true`
• `now ≥ releaseDate` (embargo) | `SELECT` filtered rows | Read event logged (anon, seriesId) | +| [**Dam Operator**](#persona-dam-operator) | POST /timeseries (gate settings) | CAC → JWT `dam_operator` | store_ts on MANUAL series | • `seriesType=="MANUAL"`
• office match
• 24 h edit window | INSERT / UPDATE; append‑lock after 24 h | Writes w/ reason; error corrections tracked | +| [**Water Manager**](#persona-water-manager) | GET/PUT (embargo override; rule curve) | CAC → JWT `water_manager`, emergency flag | Reads or modifies rule curves | • Embargo override if `emergency=true`
• office / region match | SELECT, UPDATE on rule‑curve table | Emergency flag + diff stored | +| [**Data Manager**](#persona-data-manager) | Bulk PATCH / DELETE | CAC → JWT `data_manager` | Bulk upsert or soft‑delete | • Two‑person approval for DELETE
• bulk size threshold | Batch PL/SQL jobs | Before/after diff; approver IDs | +| [**Auto‑Collection System**](#persona-auto-collection) | High‑freq POSTs (API key) | API‑Key ⇒ JWT `auto_collector` | store_ts append‑only path | • Append‑only
• rate ≤ 1 000/min
• sensor ID valid | INSERT rows only | Count & rate metrics; ingest status | +| [**Auto‑Processing System**](#persona-auto-processing) | Read raw ⇒ write CALCULATED | Service token JWT `auto_processor` | Reads, then store_ts CALCULATED | • Read raw OK
• Write only `seriesType="CALCULATED"`
• not `finalized` | INSERT derived series w/ lineage | Lineage array + calc metadata logged | +| [**External Cooperator**](#persona-external-cooperator) | Partner PUT / GET (whitelist) | OIDC ⇒ JWT `external_cooperator`, partnerId | Parameter‑scoped ops | • parameter in partner whitelist
• token expiry
• no operational data | INSERT / SELECT on partner series | Partner action + expiry stamped | +| [**Facilities Staff**](#persona-facilities-staff) | Manual POST (same as DamOp subset) | CAC ⇒ JWT `facilities_staff` | MANUAL store_ts | • `seriesType=="MANUAL"`
• shift hr (06‑18) | INSERT; 24 h self‑edit | Same audit fields as DamOp | +| [**Authorization Admin**](#persona-authorization-admin) | POST /permissions/grant | CAC/SSO ⇒ JWT `auth_admin` | Writes to policy store | • persona = `auth_admin`
• scope of grant <= office | INSERT into user_role / bundle rebuild | Grant / revoke diff, expiry | +| [**Data Steward (QA)**](#persona-data-steward) | PATCH /flags | CAC ⇒ JWT `data_steward` | Flag writer util | • Flag‑only operation
• value immutable | UPDATE flag column | Flag change w/ justification | +| [**Diagnostics Engineer**](#persona-diagnostics-engineer) | GET /diagnostics | Token ⇒ JWT `diagnostics_eng` | Log proxy endpoint | • read‑only diag persona | NO DB WRITE | Log access itself logged | +| [**Partner Data Controller**](#persona-partner-data-controller) | PATCH metadata (legalHold) | OIDC ⇒ JWT `partner_data_ctrl` | Metadata update | • partner owns series
• cannot shorten embargo | UPDATE series header | Metadata diff; partnerId | +| [**Water Quality Manager**](#persona-water-quality-manager) | GET CHEM_*; optional stats write | CAC ⇒ JWT `water_quality_mgr` | SELECT quality params; write CALCULATED | • parameter whitelist CHEM_*
• read‑only raw
• write derived only | SELECT / INSERT derived | Tagged `purpose="quality_analysis"` | + + + + + + +## Persona Use Cases + +The following use cases are organized by persona. Each use case includes a scenario, step-by-step flow, and the policy requirements that must be enforced by the authorization framework. + + +### 1. Anonymous/Public User Use Cases + +* **Primary Actor**: General public, researchers, media +* **Source**: Charles Graham interview (Community Outreach) + +#### Use Case 1.1: View Public River Levels + +```text +Scenario: Public user checks current river levels +1. User navigates to public CWMS website +2. System displays non-embargoed timeseries data +3. User views current stage/flow for public locations +4. System filters out operational/sensitive parameters + +Policy Requirements: +- No authentication required +- Only "public" classification data visible +- Embargo rules enforced (typically 7+ days old) +- No write operations permitted +``` + +#### Use Case 1.2: Download Historical Data + +```text +Scenario: Researcher downloads historical flood data +1. User searches for specific location/timeframe +2. System returns public historical timeseries +3. User exports data in CSV/JSON format +4. System logs anonymous access for statistics + +Policy Requirements: +- Rate limiting for bulk downloads +- Public data classification only +- No real-time operational data +``` + + +### 2. Dam Operator Use Cases + +* **Primary Actor**: On-site operational staff +* **Source**: Ruth Koehnke & Kaitlyn Line interviews + +#### Use Case 2.1: Manual Gate Position Entry + +```text +Scenario: Operator records morning gate settings +1. Operator authenticates with CAC/credentials +2. System verifies operator assigned to facility +3. Operator enters gate position readings +4. System validates MANUAL data source +5. Data saved with operator attribution + +Policy Requirements: +- Office-based access control +- Manual data source enforcement +- Shift-hour validation (6am-6pm) +- 24-hour modification window +- Append-only after 24 hours +``` + +#### Use Case 2.2: Correct Recent Entry + +```text +Scenario: Operator fixes data entry error +1. Operator notices error in morning entry +2. System shows entries from last 24 hours +3. Operator corrects gate position value +4. System logs modification with reason +5. Original value retained in audit trail + +Policy Requirements: +- 24-hour edit window enforcement +- Audit trail for all modifications +- Only own entries modifiable +- Justification required +``` + + +### 3. Water Manager Use Cases + +* **Primary Actor**: District water management staff +* **Source**: Jessica Batterman interview + +#### Use Case 3.1: Emergency Data Access + +```text +Scenario: Manager accesses embargoed data during flood +1. Manager authenticates with elevated privileges +2. System recognizes water_manager persona +3. Manager accesses real-time sensor data +4. System bypasses normal embargo rules +5. Access logged with emergency flag + +Policy Requirements: +- Embargo override capability +- Multi-office access if assigned +- Full read on operational data +- Emergency access logging +``` + +#### Use Case 3.2: Seasonal Operations Planning + +```text +Scenario: Manager updates seasonal rule curves +1. Manager accesses assigned reservoirs +2. System shows current operational curves +3. Manager modifies rule curve parameters +4. System validates against constraints +5. Changes require audit justification + +Policy Requirements: +- Write access to operational data +- Location/parameter restrictions +- Audit trail with justification +- No destructive operations +``` + + +### 4. Data Manager Use Cases + +* **Primary Actor**: Regional data administrators +* **Source**: Sarah Harris interview + +#### Use Case 4.1: Data Quality Control + +```text +Scenario: Admin corrects systematic sensor errors +1. Admin identifies bad sensor data pattern +2. System shows affected timeseries +3. Admin initiates bulk correction +4. System requires approval workflow +5. Corrections applied with full audit + +Policy Requirements: +- Cross-office read access +- Bulk update capabilities +- Approval workflow for changes +- Comprehensive audit logging +- Justification requirements +``` + +#### Use Case 4.2: Historical Data Cleanup + +```text +Scenario: Admin removes duplicate records +1. Admin runs duplicate detection query +2. System identifies duplicate entries +3. Admin reviews and marks for deletion +4. Supervisor approves deletion request +5. System soft-deletes with audit trail + +Policy Requirements: +- Delete permission with approval +- Two-person authorization +- Soft-delete preference +- Complete audit trail +- Regional scope limits +``` + + +### 5. Automated Collection System Use Cases + +* **Primary Actor**: SCADA systems, sensors, loggers +* **Source**: Operational requirements + +#### Use Case 5.1: High-Frequency Sensor Data + +```text +Scenario: Water level sensor reports every 15 minutes +1. Sensor authenticates with API key +2. System validates sensor registration +3. Sensor posts new water level reading +4. System enforces append-only policy +5. Data tagged as AUTOMATED source + +Policy Requirements: +- API key authentication only +- Write-only access (no read) +- Append-only enforcement +- Rate limiting (1000/minute) +- Source validation +``` + +#### Use Case 5.2: Batch Historical Upload + +```text +Scenario: Logger uploads 24 hours of data +1. Logger connects after network outage +2. System accepts batch upload +3. Logger sends array of readings +4. System validates chronological order +5. All data marked as AUTOMATED + +Policy Requirements: +- Bulk operation support +- Historical timestamp acceptance +- No modification of existing +- Sensor ID validation +``` + + +### 6. Automated Processing System Use Cases + +* **Primary Actor**: Calculation engines, models +* **Source**: System integration requirements + +#### Use Case 6.1: Flow Calculation from Stage + +```text +Scenario: System calculates flow from rating curve +1. Processor reads stage timeseries +2. System allows cross-office access +3. Processor applies rating curve +4. Calculated flow written as CALCULATED +5. Metadata includes calculation details + +Policy Requirements: +- Read access to raw data +- Cross-office for calculations +- Write CALCULATED type only +- Calculation metadata required +- Source data references +``` + +#### Use Case 6.2: Regional Aggregation + +```text +Scenario: System computes basin-wide statistics +1. Processor queries multiple offices +2. System allows regional data access +3. Processor aggregates values +4. Results stored as derived series +5. Lineage tracked to sources + +Policy Requirements: +- Multi-office read access +- Derived data write only +- Complete lineage tracking +- No raw data modification +``` + + +### 7. External Cooperator Use Cases + +* **Primary Actor**: Partner agencies (NOAA, USGS) +* **Source**: Brian Cosgrove interview + +#### Use Case 7.1: Weather Service Data Exchange + +```text +Scenario: NOAA provides precipitation forecasts +1. Partner authenticates with API key +2. System validates partnership status +3. Partner uploads forecast data +4. System restricts to allowed parameters +5. Data tagged with partner source + +Policy Requirements: +- Partnership agreement validation +- Parameter-specific access +- Time-limited credentials +- Extensive audit logging +- No operational data access +``` + +#### Use Case 7.2: Cooperative Monitoring + +```text +Scenario: USGS shares streamflow measurements +1. Partner system authenticates +2. System checks parameter whitelist +3. Partner reads/writes specific gauges +4. System enforces partnership scope +5. Access expires per agreement + +Policy Requirements: +- Whitelist-based access +- Bidirectional data sharing +- Expiring access grants +- Partnership metadata +- Audit trail emphasis +``` + + +### 8. Facilities Staff Use Cases +* **Primary Actor**: On-site facility staff (non-operator) +* **Source**: Ruth Koehnke, Brandon Kolze + +#### Use Case 8.1: Manual Sensor Reading Submission +```text +Scenario: Facilities staff enter readings from analog gauges +1. User authenticates with CAC +2. System confirms persona is `facilities_staff` +3. User enters values into manual entry screen +4. System restricts `seriesType == "MANUAL"` +5. System enforces time-of-day window (06:00–18:00) +6. Data stored with self-edit enabled for 24h +``` + +**Policy Requirements**: +- Manual-only data series enforcement +- Time-bound write window +- 24h editability, append-only thereafter +- Same audit logic as Dam Operator + + +### 9. Authorization Admin Use Cases +* **Primary Actor**: Role assignment administrator +* **Source**: Charles Graham + +#### Use Case 9.1: Grant Persona Access +```text +Scenario: Admin assigns persona to new staff +1. Admin logs in via CAC +2. Admin opens Permissions UI +3. Selects user, persona, and office +4. Submits grant request +5. System writes to user_role table and rebuilds bundle +``` + +**Policy Requirements**: +- Admin persona required +- Grant scope <= admin's own office +- Bundle rebuild trigger +- Expiry and audit metadata required + + +### 10. Data Steward (QA) Use Cases +* **Primary Actor**: QA staff or analyst +* **Source**: Jessica Batterman, Charles Graham + +#### Use Case 10.1: Apply QA Flag +```text +Scenario: Analyst flags suspect values +1. Analyst logs in via CAC +2. Opens QA UI for specific series +3. Applies `flag=reviewed` to data point +4. Justification entered +5. Audit trail stored +``` + +**Policy Requirements**: +- Only flag column editable +- Value immutability enforced +- Full audit record including justification + + +### 11. Diagnostics Engineer Use Cases +* **Primary Actor**: Developer or pipeline integrator +* **Source**: Ruth Koehnke, synthesized from Todd Boss + +#### Use Case 11.1: Ingest Pipeline Troubleshooting +```text +Scenario: Engineer checks ingest job failure +1. Authenticates with API token +2. Calls diagnostics endpoint for job status +3. System returns log trace and metrics +4. No access to underlying data +5. Access itself is logged +``` + +**Policy Requirements**: +- Read-only diagnostic persona +- No RDBMS access +- Endpoint usage logging required + + + +### 12. Partner Data Controller Use Cases +* **Primary Actor**: External partner legal/data authority +* **Source**: Brian Cosgrove + +#### Use Case 12.1: Place Legal Hold +```text +Scenario: Partner freezes dataset for legal compliance +1. Authenticates via OIDC +2. Calls metadata update API +3. Sets `legalHold=true` +4. System locks data from release +5. Action logged with partner attribution +``` + +**Policy Requirements**: +- Partner ownership attribute required +- Cannot shorten embargo +- Metadata diff logged + + + +### 13. Water Quality Manager Use Cases +* **Primary Actor**: Environmental data specialist +* **Source**: Jessica Batterman, synthesized from Todd Boss + +#### Use Case 13.1: Analyze Water Chemistry Trends +```text +Scenario: Quality manager retrieves CHEM_* time series +1. Authenticates with CAC +2. Queries CHEM_* parameters +3. System returns QA-tagged raw values +4. Optionally writes derived series (e.g., monthly average) +5. Writes must be CALCULATED type +``` + +**Policy Requirements**: +- Parameter whitelist: CHEM_* only +- Raw data read-only +- Derived writes allowed with lineage tag +- Usage tagged as `purpose="quality_analysis"` + +## System Dependencies + +### Current VPD Dependencies + +- **TimeSeriesDao.java**: Calls CWMS PL/SQL packages + - `cwms_ts.store_ts` + - `cwms_ts.retrieve_ts` + - `cwms_ts.delete_ts` +- **LocationsDao.java**: Location management procedures +- **RatingsDao.java**: Rating curve operations + +### Client Dependencies + +- **cwms-data-api-client**: Java SDK for API access +- **CWMS CAC**: Legacy thick client application +- **HEC-RAS/HEC-HMS**: Modeling software integration +- **SCADA Systems**: Direct database connections + +### Migration Considerations + +1. Maintain VPD during transition period +2. Gradual migration of client applications +3. API compatibility layer for legacy systems +4. Parallel operation capability + +## Policy Implementation Summary + +Each use case requires specific OPA policies covering: + +1. **Authentication**: Method and credential validation +2. **Persona Verification**: User-to-persona mapping +3. **Office/Region Validation**: Geographical access control +4. **Operation Authorization**: CRUD permission checking +5. **Data Classification**: Public/sensitive/operational +6. **Time-Based Rules**: Embargo, shift hours, time windows +7. **Audit Requirements**: Logging and justification needs + +The policy engine evaluates all these factors to make authorization decisions, providing the flexibility needed to support complex operational requirements while maintaining security and compliance. + diff --git a/docs/authorization-report/RptSec4-CRUDGapAnalysis.md b/docs/authorization-report/RptSec4-CRUDGapAnalysis.md new file mode 100644 index 0000000000..8a1f9df3eb --- /dev/null +++ b/docs/authorization-report/RptSec4-CRUDGapAnalysis.md @@ -0,0 +1,296 @@ +# CRUD-Permission Gap Analysis + +https://github.com/USACE/cwms-data-api/issues/1137 + +## Table of Contents + +- [CRUD-Permission Gap Analysis](#crud-permission-gap-analysis) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) + - [Q-38 Role Model and Its Limitations](#q-38-role-model-and-its-limitations) + - [Verbatim Source from Solicitation Q\&A](#verbatim-source-from-solicitation-qa) + - [Summary of Q-38 Model](#summary-of-q-38-model) + - [Core Limitations of Q-38](#core-limitations-of-q-38) + - [Implications for Transition](#implications-for-transition) + - [CRUD Operations × Persona Matrix for Timeseries](#crud-operations--persona-matrix-for-timeseries) + - [Current State (Q-38 Roles)](#current-state-q-38-roles) + - [Required State (PWS Exhibit 3 Personas)](#required-state-pws-exhibit-3-personas) + - [Extended CRUD Matrix (Including Refined Personas)](#extended-crud-matrix-including-refined-personas) + - [Missing Policy Rules for OPA Implementation](#missing-policy-rules-for-opa-implementation) + - [1. Time-Based Embargo Policy](#1-time-based-embargo-policy) + - [2. Data Type Restriction Policy](#2-data-type-restriction-policy) + - [3. Append-Only Constraint Policy](#3-append-only-constraint-policy) + - [4. Parameter-Level Access Policy](#4-parameter-level-access-policy) + - [5. Office-Based Filtering Policy](#5-office-based-filtering-policy) + - [Supplemental OPA Policy Requirements](#supplemental-opa-policy-requirements) + - [Audit and Justification Enhancements](#audit-and-justification-enhancements) + - [Timeseries Endpoint Analysis](#timeseries-endpoint-analysis) + - [Controllers Reviewed](#controllers-reviewed) + - [Current Authorization Touchpoints](#current-authorization-touchpoints) + - [Required Policy Enhancements](#required-policy-enhancements) + - [CREATE Operations (POST)](#create-operations-post) + - [READ Operations (GET)](#read-operations-get) + - [UPDATE Operations (PUT)](#update-operations-put) + - [DELETE Operations (DELETE)](#delete-operations-delete) + - [Implementation Priority Using OPA](#implementation-priority-using-opa) + - [Phase 1 - Core Policy Framework](#phase-1---core-policy-framework) + - [Phase 2 - Advanced Policy Rules](#phase-2---advanced-policy-rules) + - [Phase 3 - Policy Management \& Integration](#phase-3---policy-management--integration) + - [OPA Policy Architecture](#opa-policy-architecture) + - [OPA Policy Structure Addendum](#opa-policy-structure-addendum) + - [Findings and Path Forward](#findings-and-path-forward) + +## Overview + +This analysis examines CRUD operations across user personas, identifying gaps between the current Q-38 role model (modifier, admin) and the seven personas required by the PWS Exhibit 3. The analysis focuses primarily on timeseries endpoints, which represent the most critical data access patterns in CWMS. Our implementation will use Open Policy Agent (OPA) to define flexible, policy-based authorization rules that can evolve with business requirements. + +## Q-38 Role Model and Its Limitations + +### Verbatim Source from Solicitation Q&A + +> **Question 38:** What are the specific limitations of the current Role-Based Access Control that the new authorization model needs to address? +> +> **Answer:** +> *Our current roles are “You can modify data for a given office”, and “You can administer users in a given office” (office = district). Additionally, to prevent certain information, like required time delays from third party data, from being an issue, such data is not published to the public national systems. As we are moving towards a single shared database, we need to be able to store that data while only sharing it within the agreements, and to avoid otherwise trusted and trustworthy users from accidentally, or intentionally, altering data for which they have no responsibility.* + +### Summary of Q-38 Model + +The legacy Q-38 RBAC model defines only two privileges at the office (district) level: + +| Role | Description | +|------------------|-----------------------------------------------------------------------------| +| **Modifier** | Can create, update, and delete data for their assigned office. | +| **Administrator**| Includes modifier rights, and can also manage user roles in their office. | + +No other persona types (e.g., automated systems, QA staff, external collaborators) are formally defined, and all authorizations are based on an office-centric trust boundary. + +### Core Limitations of Q-38 + +- Insufficient granularity: cannot support distinct personas with separate read/write scopes or workflow-based constraints (e.g., embargo overrides, time-window edits). +- Office-only scope: permissions are hard-bounded to a single district; lacks safe cross-office access or regional collaboration. +- Lack of context-awareness: no conditional access (e.g., time-based embargo, emergency access, parameter-level filters). +- No automation or ABAC support: cannot represent non-human actors (e.g., ingest systems, batch processors) or enforce data-type constraints (e.g., MANUAL vs. AUTOMATED). +- Inflexibility in a shared database: as CWMS centralizes, the lack of contextual and legal-boundary enforcement becomes a liability. + +### Implications for Transition + +A persona-driven, OPA-enforced policy framework is required to meet PWS Exhibit 3 and interview-derived needs: + +- Fine-grained, context-aware CRUD operations +- Distinct personas with scoped authority +- Cross-office collaboration with data ownership enforcement +- Integration with legal agreements and embargo rules +- Auditable, non-destructive workflows for QA, automation, and partner interaction + +## CRUD Operations × Persona Matrix for Timeseries + +### Current State (Q-38 Roles) + +| Operation | Anonymous/Guest | Q-38 Modifier | Q-38 Admin | +|-----------|----------------|---------------|------------| +| **CREATE** | Denied | Full Access | Full Access | +| **READ** | Public Data Only | All Data | All Data | +| **UPDATE** | Denied | Full Access | Full Access | +| **DELETE** | Denied | Full Access | Full Access | + +### Required State (PWS Exhibit 3 Personas) + +| Operation | [Anonymous](RptSec3-UseCases.md#persona-anonymous-public) | [Dam Operator](RptSec3-UseCases.md#persona-dam-operator) | [Water Manager](RptSec3-UseCases.md#persona-water-manager) | [Data Manager](RptSec3-UseCases.md#persona-data-manager) | [Auto Collector](RptSec3-UseCases.md#persona-auto-collection) | [Auto Processor](RptSec3-UseCases.md#persona-auto-processing) | [External Cooperator](RptSec3-UseCases.md#persona-external-cooperator) | +|-----------|-----------|--------------|---------------|--------------|----------------|----------------|-------------------| +| **CREATE** | No | Manual Only | No | All Types | Append Only | Derived Only | Limited Params | +| **READ** | Public Only | No Embargo | 7-Day Embargo | All Data | Own Data | Processed | Shared Only | +| **UPDATE** | No | 24hr Window | No | All Time | No | Own Derived | No | +| **DELETE** | No | No | No | Full Access | No | No | No | + +### Extended CRUD Matrix (Including Refined Personas) + +| Operation | [Facilities Staff](RptSec3-UseCases.md#persona-facilities-staff) | [Authorization Admin](RptSec3-UseCases.md#persona-authorization-admin) | [Data Steward (QA)](RptSec3-UseCases.md#persona-data-steward) | [Diagnostics Engineer](RptSec3-UseCases.md#persona-diagnostics-engineer) | [Partner Data Controller](RptSec3-UseCases.md#persona-partner-data-controller) | [Water Quality Manager](RptSec3-UseCases.md#persona-water-quality-manager) | +|-----------|------------------|---------------------|-------------------|----------------------|-------------------------|-----------------------| +| **CREATE** | Manual Only | Policies | No | No | No | Derived Only | +| **READ** | Manual Only | All Users | QA Metadata | Logs & Diagnostics | Own Data Only | CHEM_* + Derived | +| **UPDATE** | 24hr Window | Policies | Flag Only | No | LegalHold / Metadata | Derived Stats | +| **DELETE** | No | No | No | No | No | No | + +## Missing Policy Rules for OPA Implementation + +### 1. Time-Based Embargo Policy + +- **Current**: No embargo enforcement +- **Required**: 7-day embargo for Water Managers, immediate access for Dam Operators +- **OPA Policy Need**: Implement time-based visibility rules that compare data timestamp with current time and user persona + +### 2. Data Type Restriction Policy + +- **Current**: No distinction between manual vs automated data +- **Required**: Dam Operators can only create/modify MANUAL data +- **OPA Policy Need**: Define data source type validation rules for CREATE and UPDATE operations based on user persona + +### 3. Append-Only Constraint Policy + +- **Current**: Full CRUD for all authenticated users +- **Required**: Auto Collectors can only append, never modify existing data +- **OPA Policy Need**: Implement temporal validation policy to prevent modification of historical data points + +### 4. Parameter-Level Access Policy + +- **Current**: All-or-nothing access to timeseries +- **Required**: External Cooperators limited to specific parameters +- **OPA Policy Need**: Create parameter filtering rules for both request validation and response filtering + +### 5. Office-Based Filtering Policy + +- **Current**: Enforced only at database level via VPD +- **Required**: API-level filtering for cross-office scenarios +- **OPA Policy Need**: Define office-based access rules that can be evaluated before database queries + +## Supplemental OPA Policy Requirements + +| Policy Type | Description | Personas Impacted | +|---------------------------|--------------------------------------------------------------------------------------------|----------------------------| +| Flag-Only Update | Allow PATCH only to `flag` column; prohibit changes to values or timestamps | Data Steward (QA) | +| Role Assignment Gate | Enforce that permission grants are scoped to office and persona constraints | Authorization Admin | +| Partner Metadata Control | Restrict Partner Data Controller to modify embargo metadata but block embargo shortening | Partner Data Controller | +| Observability Access | Allow access only to diagnostics endpoints and ingest logs | Diagnostics Engineer | +| Water Chemistry Filtering | Restrict access to CHEM_* parameters; allow writes only to derived series | Water Quality Manager | + +## Audit and Justification Enhancements + +All CREATE, UPDATE, and DELETE operations by non-public personas must: + +- Capture `justification` text fields on all write operations. +- Log before/after diffs for UPDATE and DELETE operations. +- Record actor persona, actor ID, and (where required) approver ID. +- Flag override scenarios (e.g., `emergency=true`) in logs. +- For DELETE: enforce two-person approval flow with time-stamped authorization metadata. + +## Timeseries Endpoint Analysis + +### Controllers Reviewed + +- **TimeSeriesController.java**: Primary CRUD operations +- **TextTimeSeriesController.java**: Text-specific operations +- **BinaryTimeSeriesController.java**: Binary data handling +- **TimeSeriesRecentController.java**: Recent data queries (embargo-sensitive) + +### Current Authorization Touchpoints + +1. **Route Registration**: Simple role check (CWMS_USERS_ROLE) +2. **Data Access**: TimeSeriesDao.java with VPD filtering +3. **No Business Rule Validation**: Missing embargo, append-only, manual-only checks + +### Required Policy Enhancements + +#### CREATE Operations (POST) + +- Define OPA policy for persona-based data type validation +- Implement source type validation policy (MANUAL vs AUTOMATED) +- Create append-only policy rules for Auto Collectors +- Define parameter access policies for External Cooperators + +#### READ Operations (GET) + +- Implement embargo filtering policy based on persona and data timestamp +- Add response filtering policies for sensitive parameters +- Define public data access policy for anonymous users + +#### UPDATE Operations (PUT) + +- Create time-window constraint policy for Dam Operators (24hr) +- Define blocking policy for Auto Collectors +- Implement data ownership validation policy for Auto Processors +- Add audit trail policy requirements for Data Managers + +#### DELETE Operations (DELETE) + +- Create policy restricting deletion to Data Managers only +- Define approval workflow policy requirements +- Implement audit log policy for deletions +- Consider soft-delete policy for compliance + +## Implementation Priority Using OPA + +### Phase 1 - Core Policy Framework + +1. Set up OPA service with basic policy structure +2. Implement embargo rule policies for timeseries reads +3. Create persona-based CRUD policy matrix +4. Define manual vs automated data validation policies + +### Phase 2 - Advanced Policy Rules + +1. Implement append-only constraint policies +2. Create time-window validation policies for updates +3. Define parameter-level filtering policies +4. Develop office-based access policies + +### Phase 3 - Policy Management & Integration + +1. Build policy testing and validation framework +2. Create policy management UI for administrators +3. Implement cross-office data sharing policies +4. Add comprehensive audit and compliance policies + +## OPA Policy Architecture + +The authorization middleware will use OPA as the policy engine, evaluating requests against a comprehensive set of Rego policies. Each policy will consider: + +1. **User Context**: Persona, office affiliation, authentication method +2. **Resource Context**: Data type, office ownership, parameter classification +3. **Operation Context**: CRUD operation, timestamp, data characteristics +4. **Environmental Context**: Current time, embargo periods, system state + +Example policy structure for timeseries embargo: + +```rego +allow { + input.operation == "read" + input.resource.type == "timeseries" + persona_allows_immediate_access[input.user.persona] +} + +allow { + input.operation == "read" + input.resource.type == "timeseries" + input.user.persona == "water_manager" + time.now_ns() - input.resource.timestamp_ns > embargo_period_ns +} +``` + +### OPA Policy Structure Addendum + +```rego +# Embargo Policy for Public and Dam Operator +allow { + input.operation == "read" + input.resource.type == "timeseries" + input.user.persona == "dam_operator" + input.resource.seriesType == "MANUAL" +} + +deny { + input.operation == "update" + input.user.persona == "auto_collector" +} + +allow { + input.operation == "read" + input.user.persona == "water_manager" + time.now_ns() - input.resource.timestamp_ns > embargo_period_ns +} + +deny { + input.operation == "delete" + not two_person_approval[input.user.id] +} +``` + +## Findings and Path Forward + +Moving from the office-centric Q-38 roles to a persona-driven model (including refined personas) unlocks the granularity CWMS needs: targeted reads/writes, context-sensitive controls, and auditable workflows. OPA provides the enforcement layer to express these as maintainable policies—embargoes, manual-only entry, append-only ingest, parameter and office scoping—without scattering logic across controllers. + +Near-term priorities to realize this design: +- Implement Phase 1 policies end-to-end: embargo filters for reads, persona-based CRUD matrix, and manual vs. automated validation in the write path. +- Add high‑value guards next (Phase 2): append‑only for Auto Collectors, 24‑hour edit window for Dam/Facilities Staff, parameter whitelists for External Cooperators. +- Wire policy decision logging and justification capture so UPDATE/DELETE diffs, emergency overrides, and two‑person approvals are provable in audits. + +This approach de-risks the shift to a shared database, supports cross‑office collaboration under clear ownership, and keeps future changes in policy—not code. diff --git a/docs/authorization-report/RptSec5-PolicyCandidates.md b/docs/authorization-report/RptSec5-PolicyCandidates.md new file mode 100644 index 0000000000..b018c572c9 --- /dev/null +++ b/docs/authorization-report/RptSec5-PolicyCandidates.md @@ -0,0 +1,482 @@ +# Candidate Policy Model Alternatives + +https://github.com/USACE/cwms-data-api/issues/1140 +https://github.com/USACE/cwms-data-api/issues/1141 + +## Table of Contents + +- [Overview](#overview) +- [Option 1: OPA-Based Policy Engine Model](#option-1-opa-based-policy-engine-model) + - [Model Architecture](#model-architecture) + - [Core Components](#core-components) + - [Policy Structure Example](#policy-structure-example) + - [Persona Implementation](#persona-implementation) + - [Q-38 Role Mapping](#q-38-role-mapping) + - [Integration Points](#integration-points) +- [Option 2: Extended RBAC/ABAC Model](#option-2-extended-rbacabac-model) + - [Model Architecture](#model-architecture-1) + - [Core Components](#core-components-1) + - [Database Schema Extensions](#database-schema-extensions) + - [Persona Implementation](#persona-implementation-1) + - [Integration Points](#integration-points-1) +- [Persona–Policy Crosswalk (Original + Refined Personas)](#persona–policy-crosswalk-original--refined-personas) +- [Model Comparison Summary](#model-comparison-summary) + - [Option 1 Advantages](#option-1-advantages) + - [Option 2 Advantages](#option-2-advantages) + - [Key Differences](#key-differences) +- [Conclusion](#conclusion) +- [Additional Technical Considerations and Migration Strategy](#additional-technical-considerations-and-migration-strategy) + +## Overview + +Based on our analysis of CWMS authorization requirements, we present two distinct policy model alternatives that combine roles, attributes, and business rules to support the seven PWS Exhibit 3 personas while maintaining compatibility with existing Q-38 roles plus the refined additional personas identified through interviews. + +See Section 3 for detailed persona definitions and use cases (anchors linked below), Section 4 for CRUD policy matrices (including the Q‑38 deep dive and verbatim source), and Section 6 for NIST‑aligned security and performance analysis. For the legacy baseline that motivates these models, see Q‑38 context in [Section 1](./RptSec1-VPD.md#the-q-38-model-background). + +## Option 1: OPA-Based Policy Engine Model + +### Model Architecture + +This model uses Open Policy Agent (OPA) with declarative Rego policies to implement a flexible, policy-as-code approach. + +#### Core Components + +- **Policy Engine**: OPA for rule evaluation +- **Policy Language**: Rego for expressing complex business logic +- **Policy Storage**: Git repositories for version control +- **Decision Cache**: High-performance caching layer +- **Integration**: Header-based context passing + +#### Policy Structure Example +> Assumes `input` shape you already use: +> ``` +> input = { +> user: { id, persona, offices, partner_id }, +> resource: { type, office, parameter, seriesType, owner_partner_id, timestamp_ns }, +> action: { op }, # "create" | "read" | "update" | "delete" +> context: { emergency }, # optional flags +> } +> ``` +##### Dam Operator Policy + +```rego +package cwms.authorization.dam_operator + +# Manual data only during shift hours +allow { + input.user.persona == "dam_operator" + input.resource.office in input.user.offices + input.resource.data_source == "manual" + within_shift_hours + within_modification_window +} +``` + +##### Water Manager Policy + +```rego +package cwms.authorization.water_manager + +# Embargo override capability +allow { + input.user.persona == "water_manager" + input.resource.office in input.user.offices + input.operation == "read" + # Can bypass embargo rules +} +``` + +##### Automated Collection System Policy + +```rego +package cwms.authorization.auto_collector + +# Append-only restrictions +allow { + input.user.persona == "auto_collector" + input.operation == "create" + input.resource.timestamp > time.now_ns() + # Cannot modify historical data +} +``` + +##### Utility Functions + +```rego +package cwms.authorization.utils + +within_shift_hours { + hour := time.clock([time.now_ns(), "America/Los_Angeles"])[0] + hour >= 6 + hour < 18 +} + +within_modification_window { + time.now_ns() - input.resource.created_at < 24 * 3600000000000 # 24 hours +} +``` +##### Facilities Staff +```rego +package cwms.authorization.facilities_staff + +default allow = false + +allow { + input.user.persona == "facilities_staff" + input.resource.type == "timeseries" + input.action.op == "create" + input.resource.seriesType == "MANUAL" + input.resource.office in input.user.offices + within_shift_hours +} + +allow { + input.user.persona == "facilities_staff" + input.resource.type == "timeseries" + input.action.op == "read" + input.resource.seriesType == "MANUAL" + input.resource.office in input.user.offices +} + +allow { + input.user.persona == "facilities_staff" + input.resource.type == "timeseries" + input.action.op == "update" + input.resource.seriesType == "MANUAL" + input.resource.office in input.user.offices + within_modification_window_24h +} + +deny { + input.user.persona == "facilities_staff" + input.action.op == "delete" +} + +within_shift_hours { + hour := time.clock([time.now_ns(), "America/Los_Angeles"])[0] + hour >= 6 + hour < 18 +} + +within_modification_window_24h { + time.now_ns() - input.resource.timestamp_ns < 24 * 3600 * 1e9 +} +``` +##### Data Steward (QA) — Flag Only +```rego +package cwms.authorization.data_steward + +default allow = false + +allow { + input.user.persona == "data_steward" + input.action.op == "read" + input.resource.type == "timeseries_flags" + input.resource.office in input.user.offices +} + +allow { + input.user.persona == "data_steward" + input.action.op == "update" + input.resource.type == "timeseries_flags" + only_flag_columns(input.resource.patch_fields) + justification_present + input.resource.office in input.user.offices +} + +deny { + input.user.persona == "data_steward" + input.action.op == "delete" +} + +only_flag_columns(fields) { + not fields[_] == "value" + not fields[_] == "timestamp" +} + +justification_present { + input.resource.justification != "" +} +``` + + +#### Persona Implementation + +| Persona | Policy Implementation | +| -------------------------- | ---------------------------------------------------------------- | +| [**Anonymous/Public**](./RptSec3-UseCases.md#persona-anonymous-public) | Public data filter with embargo rules | +| [**Dam Operator**](./RptSec3-UseCases.md#persona-dam-operator) | Manual data + shift hours + 24hr window | +| [**Water Manager**](./RptSec3-UseCases.md#persona-water-manager) | Embargo override + cross-office access | +| [**Data Manager**](./RptSec3-UseCases.md#persona-data-manager) | Full CRUD + audit requirements | +| [**Automated Collection System**](./RptSec3-UseCases.md#persona-auto-collection) | Append-only + rate limiting | +| [**Automated Processing System**](./RptSec3-UseCases.md#persona-auto-processing) | Derived data only + source validation | +| [**External Cooperator**](./RptSec3-UseCases.md#persona-external-cooperator) | Parameter whitelist + partnership scope | +| [**Facilities Staff**](./RptSec3-UseCases.md#persona-facilities-staff) | Manual data entry + shift hours + 24hr modification window | +| [**Authorization Admin**](./RptSec3-UseCases.md#persona-authorization-admin) | Scoped persona/role grants + office-based constraints | +| [**Data Steward (QA)**](./RptSec3-UseCases.md#persona-data-steward) | Flag-only edits with justification + no raw value changes | +| [**Diagnostics Engineer**](./RptSec3-UseCases.md#persona-diagnostics-engineer) | Read-only diagnostics endpoints; no DB data | +| [**Partner Data Controller**](./RptSec3-UseCases.md#persona-partner-data-controller) | Metadata updates within owned scope + embargo/hold management | +| [**Water Quality Manager**](./RptSec3-UseCases.md#persona-water-quality-manager) | Read CHEM_* parameters + derived-only writes with lineage | + + +#### Q-38 Role Mapping + +```rego +# Map Q-38 roles to new personas +q38_role_mapping := { + "modifier": ["dam_operator", "water_manager"], + "admin": ["data_manager", "cwms_admin"] +} + +# Maintain backward compatibility +allow { + input.user.q38_role == "modifier" + q38_modifier_permissions +} +``` + +### Integration Points + +#### Authorization Service Integration + +- **CdaAccessManager.java**: Receives `x-cwms-auth-context` header +- **Policy Client**: Direct OPA REST API calls +- **Configuration**: Policy bundle URLs and cache settings + +```java +// Enhanced context from OPA decisions +public class AuthorizationContext { + private String[] allowedOffices; + private Map constraints; + private Map filters; + + public boolean hasEmbargoOverride() { + return constraints.containsKey("embargo_override"); + } +} +``` + +## Option 2: Extended RBAC/ABAC Model + +### Model Architecture + +This model extends the existing database-driven role system with attribute-based access controls implemented through complex permission matrices. + +#### Core Components + +- **Role Engine**: Enhanced database tables for role definitions +- **Attribute Engine**: Attribute evaluation through stored procedures +- **Permission Matrix**: Complex database tables mapping roles to resources +- **Context Engine**: Session-based context management +- **Integration**: Direct database modifications + +#### Database Schema Extensions + +```sql +-- Enhanced role definitions +CREATE TABLE cwms_auth_enhanced_roles ( + role_id NUMBER, + role_name VARCHAR2(64), + persona_type VARCHAR2(32), + constraints JSON, + permissions JSON +); + +-- Attribute definitions +CREATE TABLE cwms_auth_attributes ( + attribute_id NUMBER, + attribute_name VARCHAR2(64), + attribute_type VARCHAR2(32), + validation_rules JSON +); + +-- Permission matrix +CREATE TABLE cwms_auth_permissions ( + permission_id NUMBER, + role_id NUMBER, + resource_type VARCHAR2(64), + operation VARCHAR2(16), + attribute_filters JSON, + time_constraints JSON +); +``` + +#### Persona Implementation + +| Persona | RBAC/ABAC Implementation | +| -------------------------- | ---------------------------------------------------------------------------- | +| [**Anonymous/Public**](./RptSec3-UseCases.md#persona-anonymous-public) | Guest role with public attribute filter | +| [**Dam Operator**](./RptSec3-UseCases.md#persona-dam-operator) | Operator role + manual_data attribute + shift_hours constraint | +| [**Water Manager**](./RptSec3-UseCases.md#persona-water-manager) | Manager role + embargo_override attribute | +| [**Data Manager**](./RptSec3-UseCases.md#persona-data-manager) | Admin role + audit_required attribute | +| [**Automated Collection System**](./RptSec3-UseCases.md#persona-auto-collection) | Collector role + append_only constraint | +| [**Automated Processing System**](./RptSec3-UseCases.md#persona-auto-processing) | Processor role + derived_data_only attribute | +| [**External Cooperator**](./RptSec3-UseCases.md#persona-external-cooperator) | Partner role + parameter_whitelist attribute | +| [**Facilities Staff**](./RptSec3-UseCases.md#persona-facilities-staff) | Facilities_staff role + manual_data attribute + shift_hours + 24hr edit window | +| [**Authorization Admin**](./RptSec3-UseCases.md#persona-authorization-admin) | Auth_admin role + scoped_grant attribute + office_constraint | +| [**Data Steward (QA)**](./RptSec3-UseCases.md#persona-data-steward) | Data_steward role + flag_only_update attribute + justification_required | +| [**Diagnostics Engineer**](./RptSec3-UseCases.md#persona-diagnostics-engineer) | Diagnostics_eng role + diagnostics_readonly scope; endpoint-only (no DB) | +| [**Partner Data Controller**](./RptSec3-UseCases.md#persona-partner-data-controller) | Partner_data_ctrl role + embargo_metadata_control attribute + ownership_enforcement | +| [**Water Quality Manager**](./RptSec3-UseCases.md#persona-water-quality-manager) | Water_quality_mgr role + parameter_whitelist (CHEM_*) + derived_only attribute | + + +## Persona–Policy Crosswalk (Original + Refined Personas) + +| Persona | Derived From | Key CRUD Permissions | Attribute / Constraint Rules | Option 1 (OPA) Implementation | Option 2 (RBAC/ABAC) Implementation | +|---------|--------------|----------------------|------------------------------|--------------------------------|--------------------------------------| +| [**Anonymous/Public**](./RptSec3-UseCases.md#persona-anonymous-public) | PWS Exhibit 3 | **C:** No, **R:** Public only, embargoed, **U/D:** No | `classification=public`; embargo > N days | Rego policy: public filter + embargo function | DB: public attribute + embargo constraint in permission matrix | +| [**Dam Operator**](./RptSec3-UseCases.md#persona-dam-operator) | PWS Exhibit 3 | **C:** Manual only, **R:** Assigned office, **U:** 24 h window, **D:** No | `seriesType=MANUAL`; shift hours; self-edit window | Rego policy modules: `within_shift_hours`, `within_modification_window` | Role row + attribute JSON for seriesType, shift hours, time constraint | +| [**Water Manager**](./RptSec3-UseCases.md#persona-water-manager) | PWS Exhibit 3 | **C:** No, **R:** Embargo override, multi-office, **U/D:** Limited | `emergency=true` override; office list | Rego: embargo override flag, multi-office match | Permission matrix: embargo_override attribute | +| [**Data Manager**](./RptSec3-UseCases.md#persona-data-manager) | PWS Exhibit 3 | **C/R/U/D:** Full CRUD with approvals | Two-person delete; audit justification | Rego: delete rule with `approvers` array; audit log enforcement | DB trigger for approval; audit table linkage | +| [**Automated Collection System**](./RptSec3-UseCases.md#persona-auto-collection) | PWS Exhibit 3 | **C:** Append only, **R:** Own data, **U/D:** No | `append_only`; rate limit; valid sensor ID | Rego: append-only check; rate limiter | SP: insert-only logic; rate limit in DB proc | +| [**Automated Processing System**](./RptSec3-UseCases.md#persona-auto-processing) | PWS Exhibit 3 | **C:** Derived only, **R:** Raw allowed, **U/D:** Derived only | `seriesType=CALCULATED`; lineage required | Rego: derived-only rule + lineage check | DB: insert constraint for CALCULATED type | +| [**External Cooperator**](./RptSec3-UseCases.md#persona-external-cooperator) | PWS Exhibit 3 | **C/R:** Parameter whitelist; **U/D:** No | `param in partner_whitelist`; expiry date | Rego: whitelist array lookup; expiry check | DB: join on partner/parameter table | +| [**Facilities Staff**](./RptSec3-UseCases.md#persona-facilities-staff) | Refined | **C:** Manual only, **R:** Manual only, **U:** 24 h window, **D:** No | `seriesType=MANUAL`; time-of-day constraint | Rego: inherits Dam Operator rules + shift hour strictness | Role+attr: seriesType, shift hours | +| [**Authorization Admin**](./RptSec3-UseCases.md#persona-authorization-admin) | Refined | **C/R/U:** Policy assignments, **D:** No | Scope ≤ own office; grant persona | Rego: role-assignment policy with scope filter | DB: grant_role proc with office constraint | +| [**Data Steward (QA)**](./RptSec3-UseCases.md#persona-data-steward) | Refined | **C:** No, **R:** QA metadata, **U:** Flag-only, **D:** No | Value immutability; justification required | Rego: patch-flag-only enforcement | DB: update proc limited to flag column | +| [**Diagnostics Engineer**](./RptSec3-UseCases.md#persona-diagnostics-engineer) | Refined | **C:** No, **R:** Logs/diag only, **U/D:** No | Endpoint-only access; no DB data | Rego: allow if `resource_type=diagnostics` | DB: N/A (API-level control only) | +| [**Partner Data Controller**](./RptSec3-UseCases.md#persona-partner-data-controller) | Refined | **C/R/U:** Metadata (legal hold), **D:** No | Cannot shorten embargo; must own data | Rego: embargo_min rule; ownership check | DB: update proc w/ embargo check | +| [**Water Quality Manager**](./RptSec3-UseCases.md#persona-water-quality-manager) | Refined | **C:** Derived only, **R:** CHEM_* only, **U:** Derived only, **D:** No | Param whitelist: CHEM_*; lineage tag | Rego: param match; derived-only write | DB: constraint on parameter name | + +Note: See Section 4 for the extended CRUD matrix and policy nuances per persona. + + +#### Policy Logic Examples + +```sql +-- Dam Operator permission check +CREATE OR REPLACE FUNCTION check_dam_operator_access( + p_user_id VARCHAR2, + p_resource_office VARCHAR2, + p_data_source VARCHAR2, + p_operation VARCHAR2 +) RETURN NUMBER IS +BEGIN + -- Check role assignment + IF NOT user_has_role(p_user_id, 'DAM_OPERATOR') THEN + RETURN 0; -- DENY + END IF; + + -- Check office assignment + IF NOT user_assigned_to_office(p_user_id, p_resource_office) THEN + RETURN 0; -- DENY + END IF; + + -- Check data source restriction + IF p_data_source != 'MANUAL' THEN + RETURN 0; -- DENY + END IF; + + -- Check shift hours + IF NOT within_shift_hours() THEN + RETURN 0; -- DENY + END IF; + + RETURN 1; -- ALLOW +END; +``` + +### Integration Points + +#### Java API Integration + +- **CdaAccessManager.java**: Enhanced role checking with attribute evaluation +- **AttributeEvaluator.java**: New component for attribute-based decisions +- **Database Procedures**: Complex stored procedures for permission checking + +```java +public class EnhancedCdaAccessManager { + public boolean isAuthorized(Context ctx, String operation, String resource) { + DataApiPrincipal principal = getDataApiPrincipal(ctx); + + // Evaluate role-based permissions + if (!hasRequiredRole(principal, resource, operation)) { + return false; + } + + // Evaluate attribute constraints + if (!satisfiesAttributeConstraints(principal, resource, operation)) { + return false; + } + + // Evaluate time-based rules + if (!satisfiesTimeConstraints(principal, resource, operation)) { + return false; + } + + return true; + } +} +``` + +## Model Comparison Summary + +### Option 1 Advantages + +- **Flexibility**: Declarative policy language handles complex rules naturally +- **Performance**: In-memory policy evaluation (<5ms decisions) +- **Maintainability**: Policy-as-code with version control and testing +- **Scalability**: Horizontal scaling independent of database + +### Option 2 Advantages + +- **Familiarity**: Extends existing database-driven security model +- **Database Integration**: Leverages existing Oracle VPD infrastructure +- **Transactional Consistency**: Authorization decisions within database transactions +- **Legacy Compatibility**: Minimal changes to existing role concepts + +### Key Differences + +| Aspect | Option 1 (OPA) | Option 2 (RBAC/ABAC) | +| ------------------------- | ---------------------- | --------------------------- | +| **Policy Storage** | Git repositories | Database tables | +| **Rule Expression** | Rego language | SQL + stored procedures | +| **Performance** | <5ms (cached) | 20-50ms (database queries) | +| **Complexity Management** | Policy composition | Permission matrix explosion | +| **Testing** | Unit testable policies | Database integration tests | +| **Migration Path** | Clean separation | Gradual database evolution | + +## Conclusion + +Both models can successfully achieve the same authorization outcomes and address the PWS requirements for seven personas while maintaining Q-38 compatibility. However, the traditional Option 2 approach introduces significantly more complexity through: + +- **Database Schema Proliferation**: Multiple new tables with complex relationships and JSON constraint columns +- **Permission Matrix Explosion**: Exponential growth in permission combinations as personas and resource types expand +- **Stored Procedure Maintenance**: Complex PL/SQL code that becomes increasingly difficult to debug and modify +- **Performance Degradation**: Database-driven decisions that don't scale well under high load +- **Testing Complexity**: Integration tests requiring full database setup for policy validation + +**The complexity only increases over time** as new requirements emerge, additional personas are needed, or business rules evolve. What starts as a manageable extension of existing patterns quickly becomes an unwieldy system of interconnected database procedures and permission matrices. + +Option 1's policy-as-code approach, while requiring initial learning investment, provides a foundation that **reduces complexity over time** through composition, reusability, and declarative rule expression. This positions CWMS for long-term maintainability and scalability as authorization requirements continue to evolve. + +## Additional Technical Considerations and Migration Strategy + +Given that Option 1 (OPA-Based Policy Engine) is the recommended target model, the following considerations addresses additional technical and operational considerations. + +### Migration and Rollback Plan +OPA will be introduced in **shadow mode** alongside existing VPD enforcement, first enabling read-only decisions before extending to writes. During this phase, results from OPA and VPD will be compared in a regression harness to detect mismatches. A documented rollback sequence allows reversion to VPD enforcement on an office-by-office basis if thresholds are exceeded. + +### Authoritative Data Source +While OPA will serve as the policy decision point, persona assignments, office configurations, and core constraints remain stored in the CWMS database as the **system of record**. These tables will be periodically exported into OPA bundles to prevent drift, ensuring consistency between database and policy engine. + +### Cache Invalidation and Staleness Controls +Decision caching in OPA will be configured to achieve >95% hit ratio, with explicit invalidation triggered by changes to persona or office data. Cache purges will be scoped to affected user and office combinations, and latency SLOs will target <5 ms for cached evaluations and <20 ms for uncached requests. + +### Selective Response Filtering +Filtering will be applied only when required by policy, and performance costs will be monitored. High-cost filters will be optimized by moving constraints into pre-query logic or pushing evaluation to the database. + +### Authentication Context Parity +OPA integration will maintain parity across authentication methods (JWT, CAC/PIV, API keys, OIDC) by using the `x-cwms-auth-context` header to carry policy decisions without altering existing authentication flows. + +### Cross-Office and Derived Data Rules +Explicit rules for cross-office access and derived-data-only operations will be enforced, including lineage validation for processed datasets and scoped permissions for aggregation and processing jobs. + +### Two-Person Approval for Destructive Operations +Destructive actions such as deletions will require approval from a second authorized user, with full audit capture. Soft-delete options will be supported for compliance and recovery purposes. + +### Integration Touchpoints +Integration into Java services will be minimal, leveraging a helper library to inject policy context and apply request constraints or post-query filters where required. Policy enforcement remains header-driven to reduce code intrusion. + +### Test Data and Load Validation +Load and performance testing will be conducted with realistic seeded datasets across offices, parameter sets, and personas to validate performance targets and avoid false-positive results in policy evaluation. diff --git a/docs/authorization-report/RptSec6-NIST.md b/docs/authorization-report/RptSec6-NIST.md new file mode 100644 index 0000000000..22a33d00a5 --- /dev/null +++ b/docs/authorization-report/RptSec6-NIST.md @@ -0,0 +1,276 @@ +# NIST RMF-Aligned Security & Performance Analysis + +https://github.com/USACE/cwms-data-api/issues/1142 + +## Table of Contents + +- [NIST Risk Management Framework Compliance](#nist-risk-management-framework-compliance) + - [Security Control Alignment](#security-control-alignment) +- [Threat Model Analysis](#threat-model-analysis) + - [Option 1 (OPA) Threat Profile](#option-1-opa-threat-profile) + - [Option 2 (RBAC/ABAC) Threat Profile](#option-2-rbacabac-threat-profile) +- [Persona Misuse Scenarios & Mitigations](#persona-misuse-scenarios--mitigations) + - [High-Risk Scenarios](#high-risk-scenarios) + - [Critical Security Controls](#critical-security-controls) +- [Performance Analysis & Benchmarks](#performance-analysis--benchmarks) + - [Authorization Decision Latency](#authorization-decision-latency) + - [CdaAccessManager.java Performance Impact](#cdaaccessmanagerjava-performance-impact) + - [Database Performance Optimization](#database-performance-optimization) + - [Caching Strategy Analysis](#caching-strategy-analysis) +- [Performance Benchmarks](#performance-benchmarks) +- [Recommendations for NIST Compliance](#recommendations-for-nist-compliance) + - [Security Hardening](#security-hardening) + - [Performance Optimization](#performance-optimization) + - [Continuous Monitoring](#continuous-monitoring) +- [Conclusion](#conclusion) +- [Option 1 NIST RMF Implementation Phasing](#option-1-nist-rmf-implementation-phasing) + - [Control-to-Architecture Mapping](#control-to-architecture-mapping) + - [RMF Artifact Tracking](#rmf-artifact-tracking) + +## NIST Risk Management Framework Compliance + +Both authorization models align with NIST SP 800-171 and RMF requirements for federal information systems, providing comprehensive security controls for water management infrastructure. + +### Security Control Alignment + +| NIST Control Family | Option 1 (OPA) | Option 2 (RBAC/ABAC) | +| ------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------- | +| **Access Control (AC)** | Policy-based access decisions with audit trails | Role-based access with database controls | +| **Audit & Accountability (AU)** | Structured decision logs with policy context | Database audit logs and stored procedure traces | +| **Identification & Authentication (IA)** | JWT/API key validation with enhanced context | Traditional user authentication with attribute validation | +| **System & Communications Protection (SC)** | API-level filtering with encrypted policy storage | Database-level security with encrypted connections | + +## Threat Model Analysis + +### Option 1 (OPA) Threat Profile + +#### Security Strengths + +- **Policy Isolation**: Authorization logic separated from application code +- **Immutable Audit Trail**: Complete decision history with policy versions +- **Fail-Safe Design**: Default deny stance with explicit allow rules +- **Version Control Security**: Policy changes tracked through Git with code reviews + +#### Threat Mitigations + +- **Policy Tampering**: Git-based version control with signed commits +- **Unauthorized Access**: OPA service runs in isolated container with minimal privileges +- **Decision Bypassing**: Transparent proxy ensures all requests are evaluated +- **Policy Injection**: Rego compilation validates policy syntax before deployment + +### Option 2 (RBAC/ABAC) Threat Profile + +#### Security Strengths + +- **Database Integration**: Leverages existing Oracle security controls +- **Transactional Consistency**: Authorization decisions within database transactions +- **Familiar Patterns**: Well-understood role-based security model +- **Established Tooling**: Existing database monitoring and audit capabilities + +#### Threat Considerations + +- **SQL Injection**: Complex stored procedures increase attack surface +- **Permission Creep**: Matrix-based permissions prone to over-privileging +- **Debug Exposure**: Complex database procedures may leak sensitive information +- **Schema Vulnerabilities**: JSON constraint columns introduce parsing risks + +## Persona Misuse Scenarios & Mitigations + +See [Section 3](./RptSec3-UseCases.md) for full persona definitions and end-to-end flows. + +### High-Risk Scenarios + +| Persona | Misuse Scenario | Option 1 Mitigation | Option 2 Mitigation | +| -------------------- | ---------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------- | +| [**Data Manager**](./RptSec3-UseCases.md#persona-data-manager) | Delete embargoed data to hide information | Policy requires approval workflow + audit trail | Database procedures log all deletion attempts | +| [**Water Manager**](./RptSec3-UseCases.md#persona-water-manager) | Abuse embargo override for unauthorized access | Rate limiting + supervisor notification on overrides | Complex stored procedure validation with alerts | +| [**Automated Collection System**](./RptSec3-UseCases.md#persona-auto-collection) | Flood system with false data | API rate limiting + data validation policies | Database triggers for anomaly detection | +| [**External Cooperator**](./RptSec3-UseCases.md#persona-external-cooperator) | Access data beyond partnership scope | Parameter whitelist strictly enforced in policy | Complex permission matrix with expiring grants | + +### Critical Security Controls + +#### Option 1 Security Controls + +```rego +# Mandatory audit logging for sensitive operations +audit_required { + input.operation in ["delete", "bulk_update"] + input.resource.classification in ["sensitive", "embargoed"] +} + +# Rate limiting for automated systems +rate_limit_check { + input.user.persona == "auto_collector" + request_count_last_hour < 1000 +} +``` + +#### Option 2 Security Controls + +```sql +-- Audit trigger for sensitive operations +CREATE OR REPLACE TRIGGER audit_sensitive_ops +BEFORE DELETE OR UPDATE ON at_cwms_ts_data +FOR EACH ROW +WHEN (OLD.classification IN ('SENSITIVE', 'EMBARGOED')) +BEGIN + INSERT INTO cwms_auth_audit_log VALUES ( + user, sysdate, 'SENSITIVE_OPERATION', + :OLD.ts_code || ' - ' || :OLD.office_id + ); +END; +``` + +## Performance Analysis & Benchmarks + +### Authorization Decision Latency + +| Scenario | Option 1 (OPA) | Option 2 (RBAC/ABAC) | +| ---------------------------------- | -------------- | -------------------- | +| **Public Read** (cached) | <1ms | 15-25ms | +| **Authenticated Read** (cached) | <2ms | 20-35ms | +| **Complex Persona Rules** (cached) | <5ms | 40-80ms | +| **High-Volume Auto Collection** | <3ms | 50-150ms | +| **Cache Miss** | 10-20ms | 100-300ms | + +### CdaAccessManager.java Performance Impact + +#### Option 1 Integration + +```java +// Lightweight header parsing - minimal overhead +AuthorizationContext context = AuthorizationHelper.parseHeader( + ctx.header("x-cwms-auth-context") +); +// ~0.1ms parsing time +``` + +#### Option 2 Integration + +```java +// Complex database queries for each request +if (!checkPersonaConstraints(principal, resource, operation)) { + return false; // 20-50ms database query +} +if (!evaluateTimeBasedRules(principal, resource)) { + return false; // Additional 10-30ms query +} +``` + +### Database Performance Optimization + +#### TimeSeriesDao.java Index Requirements + +**Option 1 Requirements:** + +- Minimal additional indexes (authorization handled at API layer) +- Existing office-based indexes sufficient +- Query performance maintained + +**Option 2 Requirements:** + +```sql +-- Additional indexes for complex permission queries +CREATE INDEX cwms_auth_user_persona_idx ON cwms_auth_user_personas + (user_id, office_code, persona_code); + +CREATE INDEX cwms_auth_time_constraint_idx ON cwms_auth_permissions + (role_id, resource_type, JSON_VALUE(time_constraints, '$.embargo_hours')); +``` + +### Caching Strategy Analysis + +#### Option 1 Caching + +- **Policy Decisions**: 5-minute TTL with intelligent invalidation +- **User Context**: Session-based caching +- **Cache Hit Ratio**: >95% for typical workloads +- **Memory Usage**: ~20MB for 10,000 active users + +#### Option 2 Caching + +- **Database Result Sets**: Query-specific caching required +- **Permission Matrices**: Complex cache invalidation logic +- **Cache Hit Ratio**: ~80% due to query complexity +- **Memory Usage**: ~50MB for equivalent user base + +## Performance Benchmarks + +### High-Volume Scenarios + +**Automated Collection System (1000 req/min)** + +- **Option 1**: 2-3ms average latency, linear scaling +- **Option 2**: 45-60ms average latency, degradation under load + +**Public Data Access (5000 req/min)** + +- **Option 1**: <1ms average latency with caching +- **Option 2**: 20-30ms average latency with database caching + +**Complex Multi-Office Queries** + +- **Option 1**: Policy evaluation scales independently of data volume +- **Option 2**: Performance degrades with permission matrix complexity + +## Recommendations for NIST Compliance + +### Security Hardening + +1. **Implement comprehensive audit logging** for all authorization decisions +2. **Enable policy decision monitoring** with anomaly detection +3. **Establish regular policy reviews** with security team validation +4. **Deploy rate limiting** for automated systems and high-risk operations + +### Performance Optimization + +1. **Deploy intelligent caching** with appropriate TTL values +2. **Monitor authorization latency** with alerts for degradation +3. **Implement graceful degradation** for cache failures +4. **Establish performance baselines** for capacity planning + +### Continuous Monitoring + +1. **Track authorization decision patterns** for anomaly detection +2. **Monitor policy effectiveness** against threat scenarios +3. **Regular penetration testing** of authorization controls +4. **Automated compliance reporting** for NIST controls + +## Conclusion + +Both options meet NIST RMF requirements, but **Option 1 provides superior security posture** through: + +- **Cleaner separation of concerns** reducing attack surface +- **Better audit capabilities** with structured decision logs +- **Higher performance** enabling real-time security monitoring +- **More robust caching** supporting high-throughput scenarios + +Option 1's policy-based approach aligns better with NIST principles of defense in depth and provides the performance characteristics needed for critical water management infrastructure. + +## Option 1 NIST RMF Implementation Phasing + +To operationalize the security posture described above and align with NIST SP 800-37/800-171, the following phased plan integrates the CDA architecture, OPA-based policy model, and RMF deliverables. This plan ensures that security controls are not only defined but implemented, tested, and continuously monitored as part of the system lifecycle. + +| Phase | When | Objective | Key Actions | Primary Artifacts | +| ----- | ---- | --------- | ----------- | ----------------- | +| **1 - Kickoff & Boundary Definition** | Month 1 | Define CUI scope in the AWS-hosted national CDA hub | • Update system diagram to show DCP → OpenDCS → CDA REST only (no JDBC)
• Tag data layers: raw, QA, records
• Document GoldenGate replication boundary and CORNET removal | Boundary & Data-Flow Doc | +| **2 - Control Selection (800-171)** | Months 1-2 | Map required controls to centralized OPA-based architecture | • Re-baseline AC, AU, CM controls for single-tenant national DB
• Add AC-2(13) “Attribute-based Roles” for per-time-series permissions | 800-171 Control Matrix | +| **3 - DevSecOps & API Hardening** | Continuous | Embed security/quality gates in CI/CD | • SAST + OWASP ZAP DAST on every merge
• Swagger Lint + OpenAPI contract tests
• JSON schema validation for value-level QA flags
• IaC lint for FedRAMP guardrails | CI/CD Pipeline Config, API Spec Tests | +| **4 - Implementation (Sprint-Aligned)** | Sprints 1-N | Build & integrate selected controls | • Access Controls: Javalin AccessManager + OPA/AWS Verified Permissions with per-series attributes (raw/QA/final)
• Audit: CloudWatch JSON logs with user/office/endpoint metadata
• CM: Immutable IaC with code-review gates
• Soft Delete & Version Lock enforcement in DAO + policy engine
• Optional: prototype SAML/OIDC broker for CAC EDIPI → JWT claims | Updated diagrams, policy repo, DAO changes | +| **5 - Assessment & RMF Evidence** | Pre-UAT | Verify control operation & gather evidence | • API fuzz/pagination tests
• Nessus/CIS scans on ECS tasks
• Threat-model each authorization pattern | SAR, SSP, Threat Models | +| **6 - Authorization Package** | Handoff | Support AO review for ATO | • Live demo: least-privilege by role, audit log, soft delete restore
• Supply SAR, POA&M, OpenAPI evidence | Authorization Package | +| **7 - Continuous Monitoring** | Post-go-live | Maintain compliance posture | • Weekly dependency scans/container rebuilds
• Monthly API spec drift checks
• Quarterly role/attribute reviews (regional variations)
• Annual 800-171 self-assessment refresh | ConMon Plan, Review Minutes | + +### Control-to-Architecture Mapping + +This phased plan ties specific NIST control families to concrete CDA components: + +- **AC**: Enforced via OPA policies for persona attributes and per-series constraints. +- **AU**: Structured decision logs in CloudWatch/S3 with immutable retention. +- **CM**: Version-controlled IaC with FedRAMP guardrails and code-review enforcement. +- **IA**: CAC/PIV integration via SAML/OIDC → JWT claims for API context. +- **SC**: TLS 1.2+ for all API and OPA traffic; encrypted policy storage in S3. + +### RMF Artifact Tracking + +For each phase, artifacts will be version-controlled and linked in the System Security Plan (SSP), ensuring traceability from control selection to AO package submission. Continuous monitoring reviews will produce minutes and evidence snapshots for quarterly and annual RMF reporting. diff --git a/docs/authorization-report/RptSec7-ComparativeAnalysis.md b/docs/authorization-report/RptSec7-ComparativeAnalysis.md new file mode 100644 index 0000000000..ccde5a1153 --- /dev/null +++ b/docs/authorization-report/RptSec7-ComparativeAnalysis.md @@ -0,0 +1,347 @@ +# Comparative Analysis & Recommendation + +https://github.com/USACE/cwms-data-api/issues/1143 + +## Table of Contents + +- [Summary](#summary) +- [Context from Prior Sections](#context-from-prior-sections) +- [Implementation Options Overview](#implementation-options-overview) + - [Option 1: OPA with Policy-Based Authorization (Recommended)](#option-1-opa-with-policy-based-authorization-recommended) + - [Option 2: Traditional RBAC/ABAC Implementation](#option-2-traditional-rbacabac-implementation) +- [Why We Recommend Option 1](#why-we-recommend-option-1) +- [Why Not Traditional RBAC/ABAC/ReBAC?](#why-not-traditional-rbacabacreback) +- [Why OPA + Policy Rules?](#why-opa--policy-rules) + - [1. Handles Complex Business Logic Naturally](#1-handles-complex-business-logic-naturally) + - [2. Policy-as-Code Benefits](#2-policy-as-code-benefits) + - [3. Performance at Scale](#3-performance-at-scale) + - [4. Future-Proof Architecture](#4-future-proof-architecture) +- [Model Comparison: Security & Manageability](#model-comparison-security--manageability) + - [Security Posture](#security-posture) + - [Manageability](#manageability) +- [Persona Scorecard](#persona-scorecard) + - [Current Q-38 Role Enforcement](#current-q-38-role-enforcement) + - [PWS Exhibit 3 Persona Support](#pws-exhibit-3-persona-support) +- [Recommended Hybrid Approach](#recommended-hybrid-approach) + - [Architecture Overview](#architecture-overview) + - [Implementation Strategy](#implementation-strategy) +- [Compliance & Performance Fit](#compliance--performance-fit) +- [Conclusion](#conclusion) +## Summary + +After comprehensive analysis of authorization approaches, we present two implementation options for CWMS authorization, with our recommendation for **Option 1: OPA with Policy Rules** as the optimal approach. + +## Context from Prior Sections + +As established in Sections 1–6, CWMS authorization must meet the following critical needs: + +- **Persona-Aware Controls:** Support for all PWS Exhibit 3 personas plus expanded roles defined in Section 3, with constraints such as embargo rules, shift-hour limits, data source restrictions, and office-specific variations. +- **NIST SP 800-171 Alignment:** Direct mapping of access control, audit, configuration management, and identification/authentication requirements to system capabilities. +- **Performance Under Load:** Sub-5ms policy decisions for high-volume automated systems, maintaining throughput without database bottlenecks (see Section 6 benchmarks). +- **Future-Proof Design:** Seamless migration path from Oracle VPD to PostgreSQL RLS without policy rewrites. +- **Operational Traceability:** Comprehensive audit trails and change control for authorization logic, with policy-as-code and Git-based versioning. +- **Compliance Traceability:** Clear linkage between implemented controls, RMF phases, and PWS/SOO objectives. + +The following comparative analysis evaluates the two authorization models (Option 1: OPA with policy rules, Option 2: RBAC/ABAC) against these requirements. Full security and performance metrics are provided in [Section 6](./RptSec6-NIST.md), while persona implementation specifics are in [Section 5](./RptSec5-PolicyCandidates.md) and [Section 3](./RptSec3-UseCases.md). + + +## Implementation Options Overview + +### Option 1: OPA with Policy-Based Authorization (Recommended) + +**Approach**: Implement a transparent proxy authorization service using Open Policy Agent (OPA) with declarative policy rules written in Rego language. + +**Key Components**: + +- Authorization Service as transparent proxy +- OPA for policy evaluation +- Policy-as-code in Git repositories +- Header-based context passing to Java API +- Minimal changes to existing CWMS Data API + +### Option 2: Traditional RBAC/ABAC Implementation + +**Approach**: Extend existing role-based system with attribute-based access control using database-driven permission matrices. + +**Key Components**: + +- Database tables for roles, permissions, attributes +- Complex permission matrices +- Direct API modifications +- Session-based context management +- Extensive changes to controller logic + +## Why We Recommend Option 1 + +Option 1 provides superior flexibility, performance, and maintainability for CWMS's complex authorization requirements while minimizing changes to the existing Java API. + +## Why Not Traditional RBAC/ABAC/ReBAC? + +### RBAC (Role-Based Access Control) Limitations + +Traditional RBAC falls short for CWMS requirements: + +```text +RBAC Model: +User → Role → Permissions + +CWMS Requirement: +User → Role → Permissions × Office × Time × Data Source × Embargo Status +``` + +**Key Limitations:** + +- Cannot express time-based embargo rules +- No support for office-based filtering within roles +- Cannot handle persona-specific constraints (e.g., Dam Operators only manual data) +- Lacks context-aware decision making + +### ABAC (Attribute-Based Access Control) Challenges + +While ABAC is more flexible, implementation complexity is prohibitive: + +```text +ABAC Complexity for CWMS: +- 7 user personas × 15 resource types × 30+ offices +- Time-based rules with office-specific variations +- Data source restrictions (manual vs automated) +- Cross-office dependencies += Thousands of attribute combinations to manage +``` + +**Key Challenges:** + +- Attribute explosion for office configurations +- Difficult to debug complex attribute interactions +- No standard implementation patterns +- Performance overhead from attribute evaluation + +### ReBAC (Relationship-Based Access Control) Mismatch + +ReBAC (like Google Zanzibar) doesn't fit CWMS patterns: + +``` +ReBAC Model: +User --member-of--> Office --owns--> Resource + +CWMS Model: +User + Office + Time + DataType + Embargo → Access Decision +``` + +**Key Mismatches:** + +- CWMS authorization isn't primarily relationship-based +- Office access is assignment-based, not hierarchical +- Time-based rules don't map to relationships +- Persona constraints are behavioral, not relational + +## Why OPA + Policy Rules? + +### 1. Handles Complex Business Logic Naturally + +```rego +# OPA elegantly expresses CWMS requirements +allow { + # Basic role and office check + input.user.roles[_] == "dam_operator" + input.resource.office in input.user.offices + + # Complex time-based constraints + within_shift_hours + + # Data source restrictions + input.resource.data_source == "manual" + + # 24-hour modification window + time.now_ns() - input.resource.created_at < 24 * 3600000000000 +} + +# Office-specific embargo rules +embargo_expired { + office_config := data.office_configs[input.resource.office] + embargo_duration := office_config.embargo_hours * 3600000000000 + time.now_ns() - input.resource.created_at > embargo_duration +} +``` + +### 2. Policy-as-Code Benefits + +**Version Control:** + +- Policies stored in Git alongside code +- Full audit trail of policy changes +- Code review process for authorization changes +- Rollback capability for policy errors + +**Testing:** + +```rego +# Testable policies +test_dam_operator_manual_data { + allow with input as { + "user": {"roles": ["dam_operator"], "offices": ["SPK"]}, + "resource": {"office": "SPK", "data_source": "manual"}, + "time": {"hour": 10} + } +} +``` + +### 3. Performance at Scale + +**OPA Performance Characteristics:** + +- In-memory policy evaluation: <1ms typical +- No database lookups during authorization +- Linear scaling with horizontal deployment +- Intelligent caching of policy decisions + +**Comparison:** +| Approach | Decision Latency | Throughput | Caching Required | +|----------|-----------------|------------|------------------| +| Database RBAC | 20-50ms | 1K req/s | Heavy | +| ABAC Engine | 10-30ms | 5K req/s | Moderate | +| OPA | <5ms | 15K+ req/s | Light | + +### 4. Future-Proof Architecture + +**Database Migration Support:** + +```rego +# Same policy works with Oracle VPD or PostgreSQL RLS +office_filter := { + "oracle": sprintf("office_id IN (%s)", [offices_sql]), + "postgres": sprintf("office_id = ANY(ARRAY[%s])", [offices_sql]) +}[input.database_type] +``` + +**Cloud-Native Ready:** + +- Containerized deployment +- Kubernetes-native integration +- Service mesh compatibility +- Multi-cloud portability + +## Model Comparison: Security & Manageability + +### Security Posture + +| Aspect | Traditional RBAC/ABAC | OPA + Policies | +| -------------------------- | --------------------- | ---------------------------- | +| **Default Stance** | Often permissive | Deny by default | +| **Policy Validation** | Runtime only | Compile-time + runtime | +| **Audit Trail** | Database logs | Structured decision logs | +| **Policy Testing** | Limited | Comprehensive test framework | +| **Separation of Concerns** | Mixed with app logic | Clean separation | + +### Manageability + +| Aspect | Traditional RBAC/ABAC | OPA + Policies | +| ------------------ | --------------------- | --------------------- | +| **Policy Updates** | Database changes | Git commits | +| **Debugging** | Complex queries | Policy traces | +| **Documentation** | Separate docs | Self-documenting code | +| **Rollback** | Database restore | Git revert | +| **Review Process** | Manual | Code review | + +## Persona Scorecard + +### Current Q-38 Role Enforcement + +| Q-38 Role | Traditional RBAC | OPA + Policies | +| ------------ | ---------------- | ------------------------------- | +| **Modifier** | Basic support | Full support with constraints | +| **Admin** | Basic support | Granular admin capabilities | + +### PWS Exhibit 3 Persona Support + +| Persona | Traditional RBAC/ABAC | OPA + Policies | +| -------------------- | --------------------- | -------------------------- | +| **Anonymous/Public** | Difficult to model | Public data rules | +| **Dam Operator** | No time constraints | Shift hours, manual-only | +| **Water Manager** | No embargo override | Complex embargo logic | +| **Data Manager** | Basic CRUD | Audit requirements | +| **Auto Collector** | No append-only | Write constraints | +| **Auto Processor** | Limited filtering | Derived data rules | +| **External Partner** | Complex setup | Parameter whitelisting | + +## Recommended Hybrid Approach + +### Architecture Overview + +```mermaid +flowchart TB + subgraph "External Clients" + Client[Client Applications] + end + + subgraph "Authorization Service Layer" + AuthService[Authorization Service
Transparent Proxy] + OPA[OPA Policy Engine] + Cache[Decision Cache] + end + + subgraph "CWMS Data API Layer" + API[CWMS Data API] + Helper[Java Helper Library] + end + + subgraph "Database Layer" + VPD["Oracle VPD
(Transitional)"] + RLS["PostgreSQL RLS
(Future)"] + end + + Client --> AuthService + AuthService --> OPA + OPA --> Cache + AuthService -->|"Set header
x-cwms-auth-context"| API + API --> Helper + Helper -->|"Parse header & set context"| VPD + Helper -.->|"Future migration"| RLS +``` + +### Implementation Strategy + +**Phase 1: API-Level Authorization (OPA)** + +- Implement OPA for all authorization decisions +- Use transparent proxy pattern +- Maintain VPD for transitional period + +**Phase 2: Gradual VPD Migration** + +- Office-by-office migration +- Parallel operation validation +- Performance optimization + +**Phase 3: PostgreSQL Ready** + +- Same OPA policies work with RLS +- Minimal code changes required +- Cloud-native deployment + +## Compliance & Performance Fit + +The comparative analysis above is reinforced by the detailed security, performance, and persona alignment findings in Section 6. Key outcomes include: + +- **NIST 800-171 & RMF Alignment**: Option 1’s policy-as-code approach directly satisfies AC, AU, IA, and SC control families, with clear traceability from implementation artifacts to control objectives. +- **Persona Coverage**: All PWS Exhibit 3 personas plus the expanded set from Section 3 (Facilities Staff, Authorization Admin, Data Steward (QA), Diagnostics Engineer, Partner Data Controller, Water Quality Manager) are fully supported, with explicit constraints for embargoes, shift limits, data source restrictions, and office-specific rules. +- **Performance Benchmarks**: Consistently delivers <5ms decision latency under high-volume automated collection scenarios and maintains >95% cache hit ratio, avoiding database bottlenecks described in Option 2. +- **Migration Readiness**: Policy definitions remain portable across Oracle VPD (transitional) and PostgreSQL RLS (future), reducing rework during database modernization. +- **Operational Assurance**: Git-based policy management ensures version-controlled changes, code-reviewed authorization updates, and immutable audit trails, aligning with RMF continuous monitoring requirements. +- **Resilience Under Load**: Tested in high-volume ingestion and multi-office query scenarios, maintaining throughput and predictable response times. + +This evidence confirms that Option 1 is not only the most technically capable but also the lowest risk path for meeting CWMS’s security, compliance, and performance objectives. + + +## Conclusion + +OPA with policy rules provides the optimal solution for CWMS authorization needs: + +1. **Flexibility**: Handles complex business rules that RBAC/ABAC cannot express +2. **Performance**: Sub-5ms decisions with minimal caching requirements +3. **Maintainability**: Policy-as-code with version control and testing +4. **Future-Proof**: Supports both Oracle VPD and PostgreSQL RLS migration +5. **Security**: Default-deny stance with comprehensive audit trails + +The policy-driven approach enables CWMS to implement sophisticated authorization rules while maintaining clean separation of concerns and preparing for future cloud migration. This positions the system for long-term success and adaptability as requirements evolve. + diff --git a/docs/authorization-report/RptSec8-ImplementationAndUI.md b/docs/authorization-report/RptSec8-ImplementationAndUI.md new file mode 100644 index 0000000000..31199bdb86 --- /dev/null +++ b/docs/authorization-report/RptSec8-ImplementationAndUI.md @@ -0,0 +1,218 @@ +# Implementation & UI Plan + +https://github.com/USACE/cwms-data-api/issues/1144 + +## Table of Contents + +- [Preamble](#preamble) +- [Implementation Options Overview](#implementation-options-overview) +- [Option A: CLI-Based Management (Recommended)](#option-a-cli-based-management-recommended) + - [In Scope](#in-scope) + - [Out of Scope](#out-of-scope) +- [Option B: Full Web-Based Management](#option-b-full-web-based-management) + - [In Scope](#in-scope-1) + - [Out of Scope](#out-of-scope-1) + - [Key Web UI Components](#key-web-ui-components) +- [Recommendations](#recommendations) + +## Preamble + +This section translates the chosen authorization architecture ([Section 7](./RptSec7-ComparativeAnalysis.md), Option 1: OPA with policy rules) into an actionable delivery plan. It keeps alignment with [Sections 1–5](./RptSec3-UseCases.md) on personas and requirements, with [Section 4](./RptSec4-CRUDGapAnalysis.md) on CRUD matrices, and with [Section 6](./RptSec6-NIST.md) on NIST/RMF phasing and evidence. Both options below deliver identical authorization behavior and compliance traceability. They differ only in the sophistication of administrative tooling and the level of self-service for non-technical users. + +## Implementation Options Overview + +Based on our analysis, we present two implementation approaches that balance scope and value delivery. Both options include the core Authorization Service with OPA integration, but differ in management interface sophistication. *Option A* (recommended) delivers the project within scope and budget, while *Option B* requires an estimated 65% more level of effort. + +## Option A: CLI-Based Management (Recommended) + +### In Scope + +#### Core Authorization Infrastructure + +- **Authorization Service**: Transparent proxy with OPA integration +- **Policy Engine**: Complete OPA setup with Rego policies +- **Helper Library**: Java integration library for CWMS Data API +- **Docker Environment**: Local development setup with Podman + +#### Resource Coverage + +- **Timeseries APIs**: Complete authorization for all timeseries endpoints +- **User Management**: Core user/group/role functionality via CLI +- **API Key Management**: Generation, validation, and lifecycle management + +#### Persona Management + +- Supports all PWS Exhibit 3 personas plus expanded roles from Section 3 (Facilities Staff, Authorization Admin, Data Steward (QA), Diagnostics Engineer, Partner Data Controller, Water Quality Manager). +- Enforces persona constraints such as embargo windows, shift-hour limits, data source restrictions, and office scoping. +- CLI commands for persona assignment, validation, and export/import of persona mappings. + +#### Compliance Alignment + +- Policy-as-code in Git with code reviews. Uses existing logging for audit evidence export (JSON/CSV) without introducing new persistent audit storage. +- Role separation for policy editing vs deployment to support least privilege. +- Control coverage aligns with NIST 800-171 AC, AU, IA, and SC families referenced in Section 6. + +#### Database Migration Support + +- Policy bundles deployable against Oracle VPD (transitional) and PostgreSQL RLS (future) without policy rewrites. +- Supports parallel run and office-by-office cutover via CLI-driven export/import. +- Includes switches for target database mode during migration testing and rollback. + +#### Administration Interface + +- **Command-Line Tool**: Comprehensive CLI for all management operations +- **Policy Management**: Git-based policy storage and deployment +- **Basic React Web Viewer**: Read-only React-based interface for permission visualization +- **Essential Documentation**: Setup guides and usage examples + +#### Integration Points + +- **CdaAccessManager.java**: Header parsing and context integration +- **TimeSeriesController.java**: Authorization context utilization +- **Controllers.java**: Helper library integration across endpoints +- **Database Schema**: Minimal table additions for user/persona management + +### Out of Scope + +- **Full Web UI**: No comprehensive web-based management interface +- **Advanced Analytics**: No usage reporting or audit dashboards +- **Bulk Operations**: No web-based bulk user/permission management +- **Audit Trail Storage**: Minimal audit trail as part of logging only, no persistent storage +- **Real-time Monitoring**: Basic logging only, no advanced monitoring UI +- **Mobile Interface**: CLI and basic web viewer only +- **Approval Workflows**: No approval process for operations +- **Real-time Notifications**: No real-time alert system + +## Option B: Full Web-Based Management + +### In Scope + +#### Everything from Option A, Plus: + +- **React-Based Admin UI**: Complete web interface for non-technical administrators +- **User Management Interface**: CRUD operations with search and filtering +- **Permission Management**: Visual permission assignment and role management +- **API Key Dashboard**: Web-based key management with usage statistics +- **Policy Management UI**: Policy editor with syntax highlighting and testing +- **Audit Trail Viewer**: Web interface for authorization decision logs +- **Reporting Dashboard**: Basic usage and access pattern reports + +#### Enhanced Web Features + +- **Visual Permission Matrix**: Interactive grid showing user-resource-operation permissions +- **Bulk Operations**: Import/export users, batch permission updates +- **Advanced Search**: Complex filtering across users, permissions, and audit logs + +#### Additional Integration Points + +- **PermissionsController.java**: REST API endpoints for web UI +- **Access Management Service**: Backend service for CRUD operations +- **Enhanced Database Schema**: Additional tables for UI state and workflows +- **API Documentation**: Complete OpenAPI specification for management endpoints + +### Out of Scope + +- **Mobile-Responsive Design**: Desktop web interface only +- **Advanced Analytics**: No complex reporting or data visualization +- **Integration APIs**: No external system integration beyond basic LDAP/AD +- **Advanced Monitoring**: Basic dashboards only +- **Approval Workflows**: No approval process for operations +- **Real-time Notifications**: No real-time alert system + +### Key Web UI Components + +#### User Management Interface + +- User list with search, filtering, and pagination +- User creation/editing forms with persona assignment +- Permission visualization for individual users +- Bulk import/export functionality + +#### Permission Management Interface + +- Visual permission matrix with role-based views +- Office-based permission scoping +- Template-based permission sets + +#### Policy Management Interface + +- Web-based Rego policy editor with syntax highlighting +- Policy testing interface with sample data +- Policy deployment pipeline with rollback capability +- Policy version history and change tracking + +## Recommendations + +**Option A** provides complete core functionality with efficient management via CLI tools, suitable for technical administrators and rapid deployment. + +**Option B** adds comprehensive web-based management interfaces for non-technical administrators, providing enhanced usability and enterprise-grade management capabilities. This adds about 65% more effort to the project. + +Both options deliver the same authorization functionality and security posture, differing only in administrative interface sophistication. + +# Appendix A. Policy–Data One-Pager (DBA Reference) + +**Scope:** Option 1 (OPA) is the decision. DB is the system of record for personas and office config. No persistent audit store in current scope; decision audit table is Phase 5+ optional. + +## Authoritative Tables + +- `cwms_auth_user_personas` (authoritative persona assignments) + - `user_id`, `persona_code`, `office_code`, `effective_date`, `expiry_date`, `constraints JSON` + - PK `(user_id, persona_code, office_code)`; index `(user_id, office_code)` +- `cwms_auth_office_config` + - `office_code`, `embargo_hours`, `timezone`, `manual_entry_window_hours` + - PK `office_code` +- `cwms_ts_series` (existing) — **add**: `series_type VARCHAR2(16) CHECK ('RAW','MANUAL','CALCULATED')` + - Optional/likely existing: `owner_partner_id`, `embargo_until`, `legal_hold` +- `at_cwms_ts_data` (existing) — **add**: `entry_method VARCHAR2(16)`, `source_system VARCHAR2(64)`, optional `data_source VARCHAR2(16)` + - Use `entry_method` for per-point provenance. Use series-level `series_type` for derived-only rules. +- `cwms_ts_flags` (existing) — QA flags table (e.g., `qa_flag`, `justification`, `updated_by`, `updated_at`) +- `cwms_auth_sync_state` (optional) + - `user_id`, `last_policy_sync` (telemetry only; not used in decisions) +- `cwms_auth_decisions` (Phase 5+ optional) + - Persistent decision audit. Not in current Section 9 scope. + +## Policy-to-Field Matrix + +| Policy/Rule | Rego module cue | Reads from (table.column) | Purpose in decision | Index/constraint guidance | Who writes/updates | +|---|---|---|---|---|---| +| Persona scoping | `data.cwms.authorization.*` persona checks | `cwms_auth_user_personas.user_id, persona_code, office_code, constraints` | Determine persona and office scope for request | PK + `(user_id, office_code)` index | Auth Admin via CLI/UI | +| Office embargo window | `embargo_expired`, office cfg | `cwms_auth_office_config.embargo_hours, timezone` | Compute office-specific embargo duration | PK on `office_code` | Security Admin | +| Shift-hour limits | `within_shift_hours` | `cwms_auth_office_config.timezone`, `manual_entry_window_hours` | Gate create/update by local shift window | PK on `office_code` | Security Admin | +| Dam Operator manual-only | dam_operator | `at_cwms_ts_data.entry_method='MANUAL'` OR series-level `cwms_ts_series.series_type='MANUAL'` | Block non-manual sources | Index `(series_id, ts)` on data; constraint on `series_type` | Ingest pipeline sets `entry_method`; Data Manager sets series metadata | +| 24h modification window | `within_modification_window_24h` | Prefer `at_cwms_ts_data.ingested_at` (if present). Fallback: `at_cwms_ts_data.ts` | Restrict updates shortly after ingestion | Index `(series_id, ts)`; consider adding `ingested_at` with default | Ingest pipeline | +| Water Manager embargo override | water_manager | `cwms_auth_user_personas.constraints.embargo_override` (JSON) | Allow read despite embargo | JSON check constraint on `constraints` | Auth Admin | +| Auto Collector append-only | auto_collector | Operation only (create allowed), provenance optional | Deny update/delete for collector persona | N/A | System role only | +| Auto Processor derived-only | auto_processor | `cwms_ts_series.series_type='CALCULATED'` | Only derived series may be written | CHECK on `series_type` | Data Manager/Processing jobs | +| External/Partner scoping | external_partner | `cwms_auth_user_personas.constraints.partner_whitelist`, `cwms_ts_series.owner_partner_id` | Enforce partner param whitelist and ownership | Index series owner; JSON check on constraints | Auth Admin sets constraints; Data Manager sets owner | +| Data Steward flag-only | data_steward | `cwms_ts_flags.*` only; must not touch value columns | Allow QA flags edits with justification | Index `(series_id, ts)` on flags | Data Steward | +| Facilities Staff limits | facilities_staff | `entry_method`, office scope, shift window | Manual-only, shift-bound create/update | See above indices | Auth Admin + ingest pipeline | +| Diagnostics Engineer read-only | diagnostics_eng | N/A (endpoint gated, no DB read rules) | Allow diagnostics endpoints only | N/A | N/A (API-only) | +| Partner Data Controller legal hold/embargo min | partner_data_ctrl | `cwms_ts_series.owner_partner_id, legal_hold, embargo_until` | Own-series metadata edits; cannot shorten embargo | Index `(owner_partner_id)`; CHECK on legal hold | Partner controller via admin flow | +| Water Quality Manager CHEM_* derived-only | water_quality_mgr | `cwms_ts_series.series_type`, parameter namespace (app-level), flags/lineage (app-level) | Read CHEM_*; writes only to derived with lineage | DB CHECK on `series_type`; param filter enforced in API | Data Manager + WQM | + +> Note: If `ingested_at` does not exist on `at_cwms_ts_data`, add it (`TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL`) so the 24h window is based on ingestion time, not the measurement timestamp. + +## Minimal Index & Constraint Checklist + +- `cwms_auth_user_personas`: PK `(user_id, persona_code, office_code)`, index `(user_id, office_code)`, JSON check on `constraints`. +- `cwms_auth_office_config`: PK `office_code`. +- `cwms_ts_series`: CHECK on `series_type`, index `(owner_partner_id)` if Partner rules are used; consider index `(series_id)` if not present. +- `at_cwms_ts_data`: composite index `(series_id, ts)`; add `ingested_at` if you enforce 24h windows by ingestion. +- `cwms_ts_flags`: index `(series_id, ts)` for QA updates. + +## Writers and Data Flow + +- **Auth Admin** (CLI/UI): upserts `cwms_auth_user_personas` (personas, office scope, constraints). +- **Security Admin**: maintains `cwms_auth_office_config` (embargo hours, timezone, manual-entry window). +- **Ingest pipeline**: sets `at_cwms_ts_data.entry_method`, `source_system`, and `ingested_at` (if added). +- **Data Manager / Processing jobs**: set `cwms_ts_series.series_type`, ownership, and metadata; create derived series. +- **Data Steward**: updates `cwms_ts_flags` only (flag/justification), never raw values. + +## Notes + +- Keep persona state **only** in `cwms_auth_user_personas` to avoid drift. If you need a convenience view, create one rather than duplicating columns onto `at_sec_cwms_users`. +- Office identifiers should be consistent across app and DB. Prefer `VARCHAR2(16)` codes (e.g., 'SPK'). +- The decision audit table `cwms_auth_decisions` is Phase 5+ only. For now, rely on structured application logs for RMF evidence export. + + + diff --git a/docs/source/access-management/architecture/authorization-flow.md b/docs/source/access-management/architecture/authorization-flow.md new file mode 100644 index 0000000000..a51566d8be --- /dev/null +++ b/docs/source/access-management/architecture/authorization-flow.md @@ -0,0 +1,154 @@ +# Authorization Flow + +The request flow through the CWMS Access Management system spans from initial client authentication through data retrieval. + +## Complete Authorization Sequence + +The following diagram shows the full request lifecycle including authentication, context retrieval, policy evaluation, and data filtering. + +```mermaid +sequenceDiagram + participant c as Client + participant kc as Keycloak + participant proxy as Authorization Proxy + participant redis as Redis + participant opa as OPA + participant api as CWMS Data API + participant db as Oracle Database + + c->>kc: Authenticate with credentials + kc-->>c: JWT Token + + c->>proxy: Request + JWT + proxy->>proxy: Extract username from JWT + + proxy->>redis: Check cache for user context + alt Cache Hit + redis-->>proxy: User context + else Cache Miss + proxy->>api: GET /user/profile + JWT + api->>kc: Validate JWT via JWKS + api->>db: Query principle_name mapping + api->>db: Query offices and roles + api-->>proxy: User profile + proxy->>redis: Cache user context (30 min TTL) + end + + proxy->>opa: Evaluate policy with user context + opa-->>proxy: Decision and constraints + + alt Request Allowed + proxy->>api: Forward request + x-cwms-auth-context header + api->>db: Execute query with WHERE constraints + db-->>api: Filtered data + api-->>proxy: Response + proxy-->>c: Response + else Request Denied + proxy-->>c: 403 Forbidden + end +``` + +## Flow Phases + +### Phase 1: Authentication + +The client authenticates with Keycloak using their credentials. Upon successful authentication, Keycloak issues a JWT token containing: + +- Issuer claim identifying the Keycloak realm +- Subject claim with the user's unique identifier +- Expiration and other standard JWT claims + +The client includes this token in subsequent requests to the authorization proxy. + +### Phase 2: User Context Resolution + +When the proxy receives a request, it extracts the username from the JWT token and attempts to retrieve the user's context. + +| Step | Description | +|------|-------------| +| Cache Check | Proxy queries Redis for cached user context | +| Profile Request | On cache miss, proxy requests user profile from CWMS Data API | +| JWT Validation | API validates the JWT using Keycloak's JWKS endpoint | +| Identity Mapping | API maps the JWT subject to a CWMS database user | +| Context Assembly | API queries user offices and roles from database | +| Cache Storage | Proxy caches the context in Redis for 30 minutes | + +The user context includes: + +- User identifier and username +- Assigned roles from the CWMS security groups +- Office affiliations and primary office +- Any additional profile attributes + +### Phase 3: Policy Evaluation + +The proxy sends the user context along with request details to OPA for policy evaluation. + +```mermaid +sequenceDiagram + participant proxy as Authorization Proxy + participant opa as OPA + + proxy->>opa: Policy input + Note right of proxy: user context, HTTP method, path, parameters + opa->>opa: Evaluate Rego policies + opa-->>proxy: Policy decision + Note left of opa: allow, constraints, embargo rules +``` + +OPA evaluates the request against configured policies and returns: + +| Field | Description | +|-------|-------------| +| `allow` | Boolean indicating whether request is permitted | +| `allowed_offices` | Array of office IDs the user can access | +| `embargo_rules` | Time-based restrictions per office | +| `embargo_exempt` | Whether user bypasses embargo restrictions | +| `time_window` | Historical data access limitations | +| `data_classification` | Classification levels the user can access | + +### Phase 4: Request Forwarding + +If the policy allows the request, the proxy constructs the `x-cwms-auth-context` header and forwards the request to the CWMS Data API. + +The header contains the complete authorization context in JSON format, enabling the API to apply filtering without making additional authorization queries. + +### Phase 5: Server-Side Filtering + +The CWMS Data API parses the authorization context header and applies constraints at the database query level. + +```mermaid +sequenceDiagram + participant proxy as Authorization Proxy + participant api as CWMS Data API + participant db as Oracle Database + + proxy->>api: Request + x-cwms-auth-context + Note over api: Parse header constraints + api->>db: SELECT * WHERE office_id IN ('SWT', 'SPK') + Note over db: Only matching records returned + db-->>api: Filtered results + api-->>proxy: Response with filtered data +``` + +The API uses the `AuthorizationFilterHelper` class to generate JOOQ conditions that are added to SQL WHERE clauses. This ensures filtering happens at the database level, preventing unauthorized data from ever leaving the database. + +## Whitelist Bypass Flow + +For endpoints not on the OPA whitelist, the proxy bypasses policy evaluation and forwards requests directly. + +```mermaid +sequenceDiagram + participant c as Client + participant proxy as Authorization Proxy + participant api as CWMS Data API + + c->>proxy: Request to non-whitelisted endpoint + proxy->>proxy: Check whitelist + Note over proxy: Endpoint not whitelisted + proxy->>api: Forward request directly + api-->>proxy: Response + proxy-->>c: Response +``` + +This allows administrative or health check endpoints to function without authorization overhead while still benefiting from the proxy infrastructure. diff --git a/docs/source/access-management/architecture/component-diagram.md b/docs/source/access-management/architecture/component-diagram.md new file mode 100644 index 0000000000..3f8b8bc0eb --- /dev/null +++ b/docs/source/access-management/architecture/component-diagram.md @@ -0,0 +1,245 @@ +# Component Diagram + +The CWMS Access Management system consists of several interconnected components that work together to provide authorization services. + +## System Component Diagram + +```mermaid +graph TB + subgraph External + client[Client Application] + end + + subgraph Access Management Layer + proxy[Authorization Proxy] + opa[Open Policy Agent] + redis[Redis Cache] + mgmt_ui[Management UI] + mgmt_api[Management API] + end + + subgraph Identity Layer + keycloak[Keycloak] + end + + subgraph Data Layer + cda[CWMS Data API] + db[(Oracle Database)] + end + + client --> proxy + proxy --> opa + proxy --> redis + proxy --> cda + proxy --> keycloak + cda --> keycloak + cda --> db + mgmt_ui --> mgmt_api + mgmt_api --> opa + mgmt_api --> keycloak +``` + +## Component Details + +### Authorization Proxy + +The authorization proxy is the entry point for all client requests to the CWMS Data API. + +```mermaid +graph LR + subgraph Authorization Proxy + router[Request Router] + jwt[JWT Parser] + cache[Cache Client] + policy[Policy Client] + context[Context Builder] + forward[Request Forwarder] + end + + router --> jwt + jwt --> cache + cache --> policy + policy --> context + context --> forward +``` + +| Subcomponent | Responsibility | +|--------------|----------------| +| Request Router | Routes incoming requests based on whitelist configuration | +| JWT Parser | Extracts user identity from JWT tokens | +| Cache Client | Interfaces with Redis for user context caching | +| Policy Client | Communicates with OPA for policy evaluation | +| Context Builder | Constructs the x-cwms-auth-context header | +| Request Forwarder | Forwards authorized requests to the backend API | + +### Open Policy Agent + +OPA provides policy-based authorization decisions using Rego policies. + +```mermaid +graph TB + subgraph OPA + engine[Policy Engine] + policies[Policy Bundle] + end + + subgraph Policies + main[cwms_authz.rego] + personas[Persona Policies] + helpers[Helper Functions] + end + + engine --> policies + policies --> main + main --> personas + main --> helpers +``` + +Policy structure: + +| Policy File | Purpose | +|-------------|---------| +| cwms_authz.rego | Main orchestrator policy | +| personas/public.rego | Anonymous access rules | +| personas/dam_operator.rego | Operational staff rules | +| personas/water_manager.rego | Management staff rules | +| personas/data_manager.rego | Regional manager rules | +| personas/automated_collector.rego | Data collection system rules | +| personas/automated_processor.rego | Data processing system rules | +| personas/external_cooperator.rego | External partner rules | +| helpers/offices.rego | Office metadata and relationships | +| helpers/time_rules.rego | Embargo and time window rules | + +### Redis Cache + +Redis stores user context to reduce database queries and improve response times. + +| Configuration | Value | +|---------------|-------| +| Key Format | `user:context:{username}` | +| TTL | 1800 seconds (30 minutes) | +| Max Memory | 256 MB | +| Eviction Policy | allkeys-lru | +| Persistence | AOF (append-only file) | + +### CWMS Data API + +The Java backend API provides data access with authorization filtering. + +```mermaid +graph TB + subgraph CWMS Data API + endpoints[REST Endpoints] + filter[Authorization Filter] + helper[AuthorizationFilterHelper] + jooq[JOOQ Query Builder] + end + + subgraph Database Access + db[(Oracle Database)] + end + + endpoints --> filter + filter --> helper + helper --> jooq + jooq --> db +``` + +| Component | Responsibility | +|-----------|----------------| +| REST Endpoints | Handle HTTP requests for various data types | +| Authorization Filter | Intercepts requests to extract auth context | +| AuthorizationFilterHelper | Parses header and generates SQL conditions | +| JOOQ Query Builder | Constructs filtered SQL queries | + +### Keycloak + +Keycloak provides identity management and authentication services. + +| Feature | Usage | +|---------|-------| +| User Management | Stores user credentials and attributes | +| JWT Issuance | Issues tokens upon successful authentication | +| JWKS Endpoint | Provides public keys for token validation | +| Realm Configuration | Defines client applications and roles | + +### Management Components + +The management UI and API provide administrative interfaces for the access management system. + +| Component | Technology | Port | Purpose | +|-----------|------------|------|---------| +| Management UI | React 18, Vite, Tailwind | 4200 | Web-based policy management | +| Management API | Node.js, Fastify | 3002 | Backend for management operations | + +## Network Topology + +All components communicate over a shared container network. + +```mermaid +graph TB + subgraph cwmsdb_net + proxy[Authorization Proxy
Port 3001] + opa[OPA
Port 8181] + redis[Redis
Port 6379] + mgmt_ui[Management UI
Port 4200] + mgmt_api[Management API
Port 3002] + cda[CWMS Data API
Port 7001] + keycloak[Keycloak
Port 8080] + db[Oracle Database
Port 1521] + end + + proxy --> opa + proxy --> redis + proxy --> cda + mgmt_ui --> mgmt_api + mgmt_api --> opa + mgmt_api --> keycloak + cda --> db + cda --> keycloak +``` + +## Data Flow Summary + +| Flow | Path | Data | +|------|------|------| +| Client Request | Client to Proxy | JWT token, HTTP request | +| Context Lookup | Proxy to Redis | Username key | +| Profile Request | Proxy to API | JWT token | +| Policy Check | Proxy to OPA | User context, request details | +| Data Request | Proxy to API | Auth context header, original request | +| Database Query | API to Oracle | Filtered SQL query | + +## Dependency Graph + +```mermaid +graph TD + proxy[Authorization Proxy] + opa[OPA] + redis[Redis] + cda[CWMS Data API] + keycloak[Keycloak] + db[Oracle Database] + mgmt_ui[Management UI] + mgmt_api[Management API] + + proxy --> opa + proxy --> redis + proxy --> cda + cda --> keycloak + cda --> db + mgmt_ui --> mgmt_api + mgmt_api --> opa + mgmt_api --> keycloak +``` + +Startup order: + +1. Oracle Database +2. Keycloak (depends on database for persistence) +3. Redis (no dependencies) +4. OPA (no dependencies) +5. CWMS Data API (depends on database and Keycloak) +6. Authorization Proxy (depends on Redis, OPA, and CWMS Data API) +7. Management API (depends on OPA and Keycloak) +8. Management UI (depends on Management API) diff --git a/docs/source/access-management/architecture/index.md b/docs/source/access-management/architecture/index.md new file mode 100644 index 0000000000..42f0cbb28b --- /dev/null +++ b/docs/source/access-management/architecture/index.md @@ -0,0 +1,131 @@ +# Architecture Overview + +The CWMS Access Management system uses a transparent proxy architecture to provide fine-grained authorization without modifying the core CWMS Data API. + +## High-Level Architecture + +```mermaid +graph TB + client[Client Application] + proxy[Authorization Proxy] + opa[Open Policy Agent] + redis[Redis Cache] + cda[CWMS Data API] + keycloak[Keycloak] + db[(Oracle Database)] + + client -->|JWT Token| proxy + proxy -->|Extract Username| keycloak + proxy -->|Policy Check| opa + proxy -->|Cache Lookup| redis + proxy -->|x-cwms-auth-context| cda + cda -->|Validate JWT| keycloak + cda -->|Filtered Queries| db +``` + +## Component Overview + +### Authorization Proxy + +The authorization proxy is a TypeScript application built with Fastify that intercepts all requests to the CWMS Data API. It serves as the central coordination point for authorization. + +| Aspect | Details | +|--------|---------| +| Technology | Node.js 24, TypeScript, Fastify | +| Port | 3001 | +| Function | Request interception, policy evaluation, context injection | + +Key responsibilities: + +- Extract JWT tokens from incoming requests +- Query user context from the CWMS Data API (with Redis caching) +- Evaluate authorization policies via OPA +- Construct and attach the `x-cwms-auth-context` header +- Forward authorized requests to the CWMS Data API + +The proxy uses a whitelist pattern where only specified endpoints are subject to OPA policy evaluation. Non-whitelisted endpoints bypass authorization checks and are forwarded directly. + +### Open Policy Agent + +OPA serves as the centralized policy decision point. All authorization logic resides in Rego policies evaluated by OPA. + +| Aspect | Details | +|--------|---------| +| Technology | OPA 0.68.0 | +| Port | 8181 | +| Function | Policy evaluation and constraint generation | + +Policy decisions include: + +- Whether the request is allowed (`allow: true/false`) +- Filtering constraints to apply at the database level +- Embargo rules for time-sensitive data +- Office-based access restrictions + +### Redis Cache + +Redis provides caching for user context to reduce database load and improve response times. + +| Aspect | Details | +|--------|---------| +| Technology | Redis 7.x | +| Port | 6379 | +| Function | User context caching | + +Cache characteristics: + +- Key format: `user:context:{username}` +- TTL: 1800 seconds (30 minutes) +- Performance improvement: 10x (2ms vs 20ms) +- Database load reduction: approximately 95% + +### CWMS Data API + +The Java-based CWMS Data API is the backend service that provides access to water management data stored in the Oracle database. + +| Aspect | Details | +|--------|---------| +| Technology | Java 11 | +| Port | 7001 | +| Function | Data retrieval with SQL-level filtering | + +The API parses the `x-cwms-auth-context` header and applies constraints at the SQL level using JOOQ conditions. The API does not make authorization decisions; it only enforces constraints specified by the authorization layer. + +### Keycloak + +Keycloak provides identity management and JWT token services. + +| Aspect | Details | +|--------|---------| +| Technology | Keycloak 19.0.1 | +| Port | 8080 | +| Function | Authentication, JWT issuance, token validation | + +Users authenticate with Keycloak and receive a JWT token. The token contains the issuer and subject claims used to map the user to their CWMS database identity. + +### Oracle Database + +The Oracle database stores all CWMS data along with user security information. + +| Aspect | Details | +|--------|---------| +| Technology | Oracle 23c Free | +| Port | 1521 | +| Function | Data storage and security metadata | + +Key tables and views: + +- `at_sec_cwms_users`: Maps user identities to CWMS offices +- `av_sec_users`: Provides user role information + +## Detailed Documentation + +- [Authorization Flow](authorization-flow.md): Sequence diagrams showing request processing +- [Component Diagram](component-diagram.md): Detailed component relationships + +```{toctree} +:maxdepth: 2 + +authorization-flow +component-diagram +``` diff --git a/docs/source/access-management/configuration/environment-variables.md b/docs/source/access-management/configuration/environment-variables.md new file mode 100644 index 0000000000..54c5760286 --- /dev/null +++ b/docs/source/access-management/configuration/environment-variables.md @@ -0,0 +1,199 @@ +# Environment Variables Reference + +Complete reference for all environment variables used by the CWMS Access Management system. + +## Java API Configuration + +These variables configure the CWMS Data API (Java) authorization behavior. + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `cwms.dataapi.access.management.enabled` | No | `false` | Enable access management filtering in the Java API | + +### Enabling Access Management + +The Java API ignores authorization headers by default. To enable filtering: + +```bash +# Environment variable +export cwms.dataapi.access.management.enabled=true + +# Or system property +java -Dcwms.dataapi.access.management.enabled=true -jar cwms-data-api.jar + +# Or in docker-compose +environment: + - cwms.dataapi.access.management.enabled=true +``` + +Priority order: System Property > Environment Variable > Default (`false`) + +When disabled, the `AuthorizationContextHelper` and `AuthorizationFilterHelper` classes return no-op values, allowing the API to function without the authorization proxy. + +## Proxy Configuration + +The following variables configure the Authorization Proxy (TypeScript). + +## Server Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PORT` | No | `3001` | HTTP server port | +| `HOST` | No | `0.0.0.0` | HTTP server bind address | +| `LOG_LEVEL` | No | `info` | Logging level: `trace`, `debug`, `info`, `warn`, `error`, `fatal` | +| `NODE_ENV` | No | - | Environment mode: `development`, `production` | + +## CWMS Data API Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `CWMS_API_URL` | Yes | `http://localhost:7001/cwms-data` | Base URL of the downstream CWMS Data API | +| `CWMS_API_TIMEOUT` | No | `30000` | Request timeout in milliseconds for downstream API calls | +| `CWMS_API_KEY` | No | - | API key for authenticating proxy requests to CWMS Data API | + +## OPA Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `OPA_URL` | No | `http://localhost:8181` | Open Policy Agent server URL | +| `OPA_POLICY_PATH` | No | `/v1/data/cwms/authorize` | OPA policy evaluation endpoint path | +| `OPA_WHITELIST_ENDPOINTS` | No | `["/cwms-data/timeseries","/cwms-data/offices"]` | JSON array of endpoint prefixes requiring OPA authorization | + +### OPA Whitelist Configuration + +The whitelist determines which endpoints go through OPA policy evaluation. Endpoints not in the whitelist bypass authorization and are proxied directly. + +```bash +# Single endpoint +OPA_WHITELIST_ENDPOINTS='["/cwms-data/timeseries"]' + +# Multiple endpoints +OPA_WHITELIST_ENDPOINTS='["/cwms-data/timeseries","/cwms-data/offices","/cwms-data/locations"]' + +# All endpoints (use with caution) +OPA_WHITELIST_ENDPOINTS='["/cwms-data"]' +``` + +To manage the whitelist using the configuration file: + +```bash +# Edit the whitelist file +vi opa-whitelist.json + +# Load into environment +./scripts/load-whitelist.sh + +# Restart the proxy +podman compose -f docker-compose.podman.yml down authorizer-proxy +podman compose -f docker-compose.podman.yml up -d authorizer-proxy +``` + +## Redis Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `REDIS_URL` | No | `redis://localhost:6379` | Redis connection URL for user context caching | + +### Redis URL Format + +``` +redis://[[username][:password]@][host][:port][/db-number] +``` + +Examples: + +```bash +# Local development +REDIS_URL=redis://localhost:6379 + +# With authentication +REDIS_URL=redis://user:password@redis.example.com:6379 + +# With database selection +REDIS_URL=redis://localhost:6379/1 +``` + +## Cache Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `CACHE_TTL_SECONDS` | No | `300` | Time-to-live for cached items in seconds (5 minutes default) | +| `CACHE_MAX_SIZE` | No | `1000` | Maximum number of items in the in-memory cache | + +The proxy uses a two-tier caching strategy: +1. In-memory cache for fast access (configured by these variables) +2. Redis for distributed caching across multiple proxy instances + +## Authorization Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `BYPASS_AUTH` | No | `false` | Skip authorization checks when `true` (development only) | + +Setting `BYPASS_AUTH=true` disables authorization checks. This should only be used for local development and testing. + +## Keycloak Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `KEYCLOAK_URL` | No | - | Keycloak server base URL | +| `KEYCLOAK_ADMIN_USER` | No | - | Keycloak admin username for management operations | +| `KEYCLOAK_ADMIN_PASSWORD` | No | - | Keycloak admin password | + +## Docker Compose Port Mappings + +These variables are used by docker-compose for port mapping: + +| Variable | Default | Description | +|----------|---------|-------------| +| `MANAGEMENT_UI_PORT` | `4200` | Management UI external port | +| `MANAGEMENT_API_PORT` | `3002` | Management API external port | +| `AUTHORIZER_PROXY_PORT` | `3001` | Authorization Proxy external port | +| `REDIS_PORT` | `6379` | Redis external port | +| `OPA_PORT` | `8181` | OPA external port | +| `NETWORK_NAME` | `cwmsdb_net` | Docker network name | + +## Example Configuration File + +```bash +# Server Configuration +NODE_ENV=development +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=debug + +# CWMS Data API Configuration +CWMS_API_URL=http://data-api:7000/cwms-data +CWMS_API_TIMEOUT=30000 +CWMS_API_KEY= + +# OPA Configuration +OPA_URL=http://opa:8181 +OPA_POLICY_PATH=/v1/data/cwms/authz/allow +OPA_WHITELIST_ENDPOINTS=["/cwms-data/timeseries","/cwms-data/offices"] + +# Redis Configuration +REDIS_URL=redis://redis:6379 + +# Cache Configuration +CACHE_TTL_SECONDS=300 +CACHE_MAX_SIZE=1000 + +# Authorization +BYPASS_AUTH=false + +# Keycloak Configuration +KEYCLOAK_URL=http://auth:8080/auth +KEYCLOAK_ADMIN_USER=admin +KEYCLOAK_ADMIN_PASSWORD=admin +``` + +## Production Recommendations + +| Variable | Recommendation | +|----------|----------------| +| `LOG_LEVEL` | Set to `info` or `warn` to reduce log volume | +| `BYPASS_AUTH` | Must be `false` in production | +| `CACHE_TTL_SECONDS` | Increase to `1800` (30 minutes) for better performance | +| `CWMS_API_KEY` | Generate and set a secure API key | +| `KEYCLOAK_ADMIN_PASSWORD` | Use a strong, unique password | diff --git a/docs/source/access-management/configuration/index.md b/docs/source/access-management/configuration/index.md new file mode 100644 index 0000000000..d45c70908d --- /dev/null +++ b/docs/source/access-management/configuration/index.md @@ -0,0 +1,99 @@ +# Configuration Overview + +The CWMS Authorization Proxy uses environment variables for all configuration. This approach enables flexible deployment across development, staging, and production environments without code changes. + +## Configuration System + +The proxy uses [@fastify/env](https://github.com/fastify/fastify-env) to load and validate environment variables at startup. Configuration is validated against a JSON schema, ensuring required values are present and types are correct before the server starts. + +```mermaid +flowchart LR + envFile[.env file] --> fastifyEnv[fastify-env] + environment[Environment] --> fastifyEnv + fastifyEnv --> validatedConfig[Validated Config] + validatedConfig --> application[Application] +``` + +## Configuration Categories + +| Category | Purpose | Key Variables | +|----------|---------|---------------| +| Server | HTTP server settings | `PORT`, `HOST`, `LOG_LEVEL` | +| CWMS API | Downstream API connection | `CWMS_API_URL`, `CWMS_API_TIMEOUT`, `CWMS_API_KEY` | +| OPA | Policy engine integration | `OPA_URL`, `OPA_POLICY_PATH`, `OPA_WHITELIST_ENDPOINTS` | +| Redis | User context caching | `REDIS_URL` | +| Cache | In-memory cache settings | `CACHE_TTL_SECONDS`, `CACHE_MAX_SIZE` | +| Authorization | Auth behavior control | `BYPASS_AUTH` | + +## Loading Configuration + +Configuration loads from two sources, with environment variables taking precedence: + +1. `.env` file in the application root (loaded via dotenv) +2. Process environment variables + +### Development Setup + +```bash +# Copy example configuration +cp .env.example .env + +# Edit with your local settings +vi .env +``` + +### Container Deployment + +For container deployments, pass environment variables directly: + +```bash +podman run -d \ + -e PORT=3001 \ + -e CWMS_API_URL=http://data-api:7000/cwms-data \ + -e OPA_URL=http://opa:8181 \ + -e REDIS_URL=redis://redis:6379 \ + cwms-authorizer-proxy:local-dev +``` + +Or use the docker-compose file which references the `.env` file: + +```bash +podman compose -f docker-compose.podman.yml up -d authorizer-proxy +``` + +## Applying Configuration Changes + +Configuration is read at startup. To apply changes: + +### Development Mode + +Restart the development server: + +```bash +pnpm nx serve authorizer-proxy +``` + +### Container Mode + +Recreate the container (restart alone does not reload environment variables): + +```bash +podman compose -f docker-compose.podman.yml down authorizer-proxy +podman compose -f docker-compose.podman.yml up -d authorizer-proxy +``` + +## Validation + +The proxy validates all configuration at startup. If required variables are missing or invalid, the server will fail to start with a descriptive error message. + +Required variables: +- `PORT` - Server port (has default) +- `CWMS_API_URL` - Downstream CWMS Data API URL (required, no default in production) + +## Related Documentation + +```{toctree} +:maxdepth: 1 + +environment-variables +``` diff --git a/docs/source/access-management/filtering/classification.md b/docs/source/access-management/filtering/classification.md new file mode 100644 index 0000000000..8fb9698ddc --- /dev/null +++ b/docs/source/access-management/filtering/classification.md @@ -0,0 +1,187 @@ +# Data Classification Filtering + +Data classification filtering restricts access based on the sensitivity level of data. Each data record can have a classification level, and users can only access data that matches their allowed classifications. + +## Classification Levels + +CWMS supports multiple classification levels for data: + +| Level | Description | +|-------|-------------| +| `public` | Accessible to all authenticated users | +| `internal` | Restricted to internal USACE users | +| `restricted` | Limited to specific authorized personnel | +| `confidential` | Highest sensitivity, very limited access | + +## Constraint Format + +The authorization proxy includes allowed classifications in the constraints: + +```json +{ + "constraints": { + "data_classification": ["public", "internal"] + } +} +``` + +Users with this constraint can access data classified as either "public" or "internal" but not "restricted" or "confidential". + +## Filter Behavior + +```mermaid +flowchart TD + Start[getClassificationFilter called] --> Check{data_classification exists?} + Check -->|No| NoFilter[Return noCondition] + Check -->|Yes| Empty{Array empty?} + Empty -->|Yes| Deny[Return falseCondition] + Empty -->|No| Apply[Return classification IN allowed OR classification IS NULL] +``` + +## Implementation + +The `getClassificationFilter` method generates the appropriate JOOQ condition: + +```java +public Condition getClassificationFilter(Field classificationField) { + if (constraints == null || !constraints.has("data_classification")) { + return DSL.noCondition(); + } + + JsonNode classificationNode = constraints.get("data_classification"); + List allowedClassifications = new ArrayList<>(); + + if (classificationNode.isArray()) { + for (JsonNode classification : classificationNode) { + allowedClassifications.add(classification.asText()); + } + } + + if (allowedClassifications.isEmpty()) { + return DSL.falseCondition(); + } + + // Allow matching classifications OR null (unclassified data) + return DSL.or( + classificationField.in(allowedClassifications), + classificationField.isNull() + ); +} +``` + +## Handling Unclassified Data + +The filter explicitly allows records where the classification field is `NULL`. This ensures that unclassified or legacy data remains accessible to users who have at least one classification level authorized. + +```java +return DSL.or( + classificationField.in(allowedClassifications), + classificationField.isNull() +); +``` + +## Scenarios + +### Standard User Access + +User with `data_classification: ["public", "internal"]`: + +```sql +SELECT * FROM at_cwms_ts_id +WHERE (data_classification IN ('public', 'internal') + OR data_classification IS NULL) +``` + +### Elevated Access + +User with all classification levels: + +```json +{ + "constraints": { + "data_classification": ["public", "internal", "restricted", "confidential"] + } +} +``` + +```sql +SELECT * FROM at_cwms_ts_id +WHERE (data_classification IN ('public', 'internal', 'restricted', 'confidential') + OR data_classification IS NULL) +``` + +### No Classification Access + +If the `data_classification` array is empty, all access is denied: + +```sql +SELECT * FROM at_cwms_ts_id +WHERE 1 = 0 +``` + +### No Constraint Defined + +If no `data_classification` constraint exists in the header, no filtering is applied: + +```sql +SELECT * FROM at_cwms_ts_id +-- No classification condition +``` + +## Usage Example + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +// Get classification filter +Condition classFilter = filterHelper.getClassificationFilter( + TIMESERIES.DATA_CLASSIFICATION +); + +// Apply to query +SelectQuery query = dsl.selectFrom(TIMESERIES) + .where(classFilter) + .getQuery(); +``` + +## Combined Filtering + +In practice, classification filtering is combined with other filter types using the `getAllFilters` method: + +```java +public Condition getAllFilters( + Field officeField, + Field timestampField, + Field classificationField, + String requestedOffice, + Timestamp userRequestedBeginTime) { + + if (constraints == null) { + return DSL.noCondition(); + } + + Condition officeFilter = getOfficeFilter(officeField, requestedOffice); + Condition embargoFilter = getEmbargoFilter(timestampField, officeField, requestedOffice); + Condition timeWindowFilter = getTimeWindowFilter(timestampField, userRequestedBeginTime); + Condition classificationFilter = classificationField != null + ? getClassificationFilter(classificationField) + : DSL.noCondition(); + + return DSL.and(officeFilter, embargoFilter, timeWindowFilter, classificationFilter); +} +``` + +This ensures that a record must pass all filters to be returned. + +## Generated SQL Example + +For a user with office restrictions, embargo rules, and classification limits: + +```sql +SELECT * FROM at_cwms_ts_id +WHERE office_id IN ('SWT', 'SPK') + AND version_date < TIMESTAMP '2024-01-13 10:00:00' + AND (data_classification IN ('public', 'internal') + OR data_classification IS NULL) +``` + diff --git a/docs/source/access-management/filtering/embargo-rules.md b/docs/source/access-management/filtering/embargo-rules.md new file mode 100644 index 0000000000..c5db950734 --- /dev/null +++ b/docs/source/access-management/filtering/embargo-rules.md @@ -0,0 +1,279 @@ +# Embargo Rules + +Embargo rules restrict access to recent data. Data that is newer than the embargo period is considered "embargoed" and will not be returned to users who are not exempt. This is commonly used to give data owners time to review and validate data before it becomes publicly accessible. + +## Embargo Concept + +The embargo period defines how old data must be before a user can access it. For example, a 168-hour (7-day) embargo means users can only see data that is at least 7 days old. + +```mermaid +flowchart LR + subgraph Embargoed + Recent[Recent Data
Last 7 days] + end + subgraph Accessible + Historical[Historical Data
Older than 7 days] + end + Recent -.->|Blocked| User + Historical -->|Allowed| User +``` + +## Constraint Format + +Embargo rules appear in the `x-cwms-auth-context` header in two forms: + +### Office-Based Embargo + +```json +{ + "constraints": { + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false + } +} +``` + +| Field | Description | +|-------|-------------| +| `embargo_rules.` | Hours of embargo for specific office | +| `embargo_rules.default` | Default embargo when office not specified | +| `embargo_exempt` | If true, user bypasses all embargo rules | + +### Time Series Group Embargo + +```json +{ + "constraints": { + "ts_group_embargo": { + "Streamflow": 72, + "Stage": 24, + "Precipitation": 0 + }, + "embargo_exempt": false + } +} +``` + +Time series group embargo allows different embargo periods based on data type rather than office. + +## Implementation + +### Office-Based Embargo Filter + +The `getEmbargoFilter` method applies office-based embargo rules: + +```java +public Condition getEmbargoFilter( + Field timestampField, + Field officeField, + String requestedOffice) { + + if (constraints == null) { + return DSL.noCondition(); + } + + // Check exemption first + boolean embargoExempt = constraints.has("embargo_exempt") && + constraints.get("embargo_exempt").asBoolean(); + if (embargoExempt) { + return DSL.noCondition(); + } + + JsonNode embargoRulesNode = constraints.get("embargo_rules"); + if (embargoRulesNode == null || embargoRulesNode.isNull()) { + return DSL.noCondition(); + } + + // Apply office-specific embargo + if (requestedOffice != null && embargoRulesNode.has(requestedOffice)) { + int embargoHours = embargoRulesNode.get(requestedOffice).asInt(); + Timestamp cutoff = Timestamp.from( + Instant.now().minus(embargoHours, ChronoUnit.HOURS) + ); + return timestampField.lessThan(cutoff); + } + + // Fall back to default embargo + if (embargoRulesNode.has("default")) { + int defaultHours = embargoRulesNode.get("default").asInt(); + Timestamp defaultCutoff = Timestamp.from( + Instant.now().minus(defaultHours, ChronoUnit.HOURS) + ); + return timestampField.lessThan(defaultCutoff); + } + + return DSL.noCondition(); +} +``` + +### Time Series Group Embargo Filter + +The `getTsGroupEmbargoFilter` method applies embargo based on time series group: + +```java +public Condition getTsGroupEmbargoFilter( + Field timestampField, + String tsGroupId) { + + if (constraints == null) { + return DSL.noCondition(); + } + + boolean embargoExempt = constraints.has("embargo_exempt") && + constraints.get("embargo_exempt").asBoolean(); + if (embargoExempt) { + return DSL.noCondition(); + } + + JsonNode tsGroupEmbargoNode = constraints.get("ts_group_embargo"); + if (tsGroupEmbargoNode == null || tsGroupEmbargoNode.isNull()) { + return DSL.noCondition(); + } + + if (tsGroupId != null && tsGroupEmbargoNode.has(tsGroupId)) { + int embargoHours = tsGroupEmbargoNode.get(tsGroupId).asInt(); + if (embargoHours == 0) { + return DSL.noCondition(); // Zero means no embargo + } + Timestamp cutoff = Timestamp.from( + Instant.now().minus(embargoHours, ChronoUnit.HOURS) + ); + return timestampField.lessThan(cutoff); + } + + // Default to 7 days for unknown groups + int defaultHours = 168; + Timestamp defaultCutoff = Timestamp.from( + Instant.now().minus(defaultHours, ChronoUnit.HOURS) + ); + return timestampField.lessThan(defaultCutoff); +} +``` + +## Filter Behavior + +```mermaid +flowchart TD + Start[getEmbargoFilter called] --> Exempt{embargo_exempt?} + Exempt -->|Yes| NoFilter[Return noCondition] + Exempt -->|No| Rules{embargo_rules exists?} + Rules -->|No| NoFilter + Rules -->|Yes| Office{Office specified?} + Office -->|Yes| OfficeRule{Office rule exists?} + OfficeRule -->|Yes| ApplyOffice[Apply office embargo] + OfficeRule -->|No| Default{default rule exists?} + Office -->|No| Default + Default -->|Yes| ApplyDefault[Apply default embargo] + Default -->|No| NoFilter +``` + +## Embargo Exemption + +Certain user personas are exempt from embargo rules. In OPA policy: + +```rego +embargo_exempt_personas := ["data_manager", "water_manager", "system_admin", "hec_employee"] + +user_embargo_exempt(user) if { + user.persona in embargo_exempt_personas +} +``` + +When `embargo_exempt: true` is set in constraints, no embargo filtering is applied. + +## Generated SQL Examples + +For a user with 168-hour embargo on SPK: + +```sql +SELECT * FROM at_cwms_ts_id +WHERE version_date < TIMESTAMP '2024-01-13 10:00:00' +``` + +For an exempt user: +```sql +SELECT * FROM at_cwms_ts_id +-- No embargo condition applied +``` + +## Time Window Restrictions + +Time window restrictions are the inverse of embargo rules. Instead of blocking recent data, they limit how far back a user can query historical data. This is useful for operational users who only need current data. + +### Constraint Format + +```json +{ + "constraints": { + "time_window": { + "restrict_hours": 8 + } + } +} +``` + +### Implementation + +```java +public Condition getTimeWindowFilter( + Field timestampField, + Timestamp userRequestedBeginTime) { + + if (constraints == null || !constraints.has("time_window")) { + return DSL.noCondition(); + } + + JsonNode timeWindowNode = constraints.get("time_window"); + if (timeWindowNode.isNull() || !timeWindowNode.has("restrict_hours")) { + return DSL.noCondition(); + } + + int restrictHours = timeWindowNode.get("restrict_hours").asInt(); + Timestamp cutoffTime = Timestamp.from( + Instant.now().minus(restrictHours, ChronoUnit.HOURS) + ); + + // If user requested older data, enforce cutoff + if (userRequestedBeginTime == null || userRequestedBeginTime.before(cutoffTime)) { + return timestampField.greaterOrEqual(cutoffTime); + } + + return timestampField.greaterOrEqual(userRequestedBeginTime); +} +``` + +### Comparison: Embargo vs Time Window + +| Rule Type | Blocks | Allows | Use Case | +|-----------|--------|--------|----------| +| Embargo | Recent data (newer than X hours) | Historical data | Data validation period | +| Time Window | Historical data (older than X hours) | Recent data | Operational dashboards | + +## Usage Example + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +// Get embargo filter for office-based data +Condition embargoFilter = filterHelper.getEmbargoFilter( + TIMESERIES.VERSION_DATE, + TIMESERIES.OFFICE_ID, + "SPK" +); + +// Get time window filter +Condition timeWindowFilter = filterHelper.getTimeWindowFilter( + TIMESERIES.VERSION_DATE, + userRequestedBeginTime +); + +// Combine filters +SelectQuery query = dsl.selectFrom(TIMESERIES) + .where(DSL.and(embargoFilter, timeWindowFilter)) + .getQuery(); +``` + diff --git a/docs/source/access-management/filtering/index.md b/docs/source/access-management/filtering/index.md new file mode 100644 index 0000000000..9ff9330c66 --- /dev/null +++ b/docs/source/access-management/filtering/index.md @@ -0,0 +1,112 @@ +# Data Filtering Overview + +The CWMS Access Management system enforces data access controls through database-level filtering. The authorization proxy passes filtering constraints to the Java API via the `x-cwms-auth-context` header, and the API applies these constraints directly to database queries using JOOQ conditions. + +## Architecture + +```mermaid +flowchart LR + Client --> Proxy[Authorization Proxy] + Proxy --> OPA[OPA Policy Engine] + OPA --> Proxy + Proxy --> API[Java API] + API --> Helper[AuthorizationFilterHelper] + Helper --> DB[(Oracle Database)] +``` + +The key principle is that all filtering happens at the database level. The authorization proxy determines what constraints apply to a user, but the Java API enforces them by modifying SQL queries. This ensures data never leaves the database unless the user is authorized to see it. + +## Filtering Mechanism + +The `AuthorizationFilterHelper` class parses the `x-cwms-auth-context` header and generates JOOQ `Condition` objects that are applied to WHERE clauses: + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +Condition allFilters = filterHelper.getAllFilters( + OFFICE_ID, // office field + VERSION_DATE, // timestamp field + DATA_CLASSIFICATION, // classification field + requestedOffice, // user-requested office (optional) + userRequestedBeginTime // user-requested start time (optional) +); + +SelectQuery query = dsl.selectFrom(TABLE) + .where(allFilters) + .getQuery(); +``` + +## Filter Types + +| Filter Type | Purpose | Constraint Field | +|-------------|---------|------------------| +| [Office Filtering](office-filtering.md) | Restrict access to specific offices | `allowed_offices` | +| [Embargo Rules](embargo-rules.md) | Restrict access to recent data | `embargo_rules`, `ts_group_embargo` | +| [Time Window](embargo-rules.md) | Limit historical data access | `time_window` | +| [Data Classification](classification.md) | Control access by sensitivity level | `data_classification` | + +## Authorization Context Header + +The proxy sends filtering constraints in the `x-cwms-auth-context` header as JSON: + +```json +{ + "policy": { + "allow": true, + "decision_id": "proxy-abc123" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "roles": ["cwms_user"], + "offices": ["SWT"], + "primary_office": "SWT" + }, + "constraints": { + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] + } +} +``` + +## Filter Combination + +When multiple filters apply, they are combined with AND logic: + +```java +return DSL.and(officeFilter, embargoFilter, timeWindowFilter, classificationFilter); +``` + +This means a record must pass all filters to be returned. For example, a user with office restrictions and embargo rules will only see data that: + +1. Belongs to one of their allowed offices +2. Is older than the embargo period +3. Falls within their time window +4. Matches their allowed classifications + +## Disabled Mode + +When access management is disabled (via configuration), the `AuthorizationFilterHelper` returns `DSL.noCondition()` for all filters, effectively allowing unrestricted access. This is determined by checking `AuthorizationContextHelper.isEnabled()` at construction time. + +## Related Documentation + +- [Office-Based Filtering](office-filtering.md) +- [Embargo Rules](embargo-rules.md) +- [Data Classification](classification.md) + +```{toctree} +:maxdepth: 2 + +office-filtering +embargo-rules +classification +``` diff --git a/docs/source/access-management/filtering/office-filtering.md b/docs/source/access-management/filtering/office-filtering.md new file mode 100644 index 0000000000..47b38f9a5a --- /dev/null +++ b/docs/source/access-management/filtering/office-filtering.md @@ -0,0 +1,162 @@ +# Office-Based Filtering + +Office-based filtering restricts data access based on the user's authorized offices. Each CWMS user is associated with one or more offices, and the filtering ensures they can only query data belonging to those offices. + +## How It Works + +The authorization proxy evaluates the user's office permissions and includes an `allowed_offices` array in the constraints: + +```json +{ + "constraints": { + "allowed_offices": ["SWT", "SPK", "NWD"] + } +} +``` + +The Java API uses this array to generate a JOOQ condition that filters query results. + +## Filter Behavior + +```mermaid +flowchart TD + Start[getOfficeFilter called] --> Check{allowed_offices exists?} + Check -->|No| NoFilter[Return noCondition] + Check -->|Yes| Wildcard{Contains wildcard?} + Wildcard -->|Yes| NoFilter + Wildcard -->|No| Empty{Array empty?} + Empty -->|Yes| Deny[Return falseCondition] + Empty -->|No| Requested{Office requested?} + Requested -->|Yes| Authorized{User authorized?} + Authorized -->|Yes| Single[Return office = requested] + Authorized -->|No| Deny + Requested -->|No| Multiple[Return office IN allowed] +``` + +## Implementation + +The `getOfficeFilter` method in `AuthorizationFilterHelper` handles all office filtering scenarios: + +```java +public Condition getOfficeFilter(Field officeField, String requestedOffice) { + if (constraints == null || !constraints.has("allowed_offices")) { + return DSL.noCondition(); + } + + JsonNode allowedOfficesNode = constraints.get("allowed_offices"); + List allowedOffices = new ArrayList<>(); + + if (allowedOfficesNode.isArray()) { + for (JsonNode office : allowedOfficesNode) { + allowedOffices.add(office.asText()); + } + } + + // Wildcard grants access to all offices + if (allowedOffices.contains("*")) { + return DSL.noCondition(); + } + + // Empty array denies all access + if (allowedOffices.isEmpty()) { + return DSL.falseCondition(); + } + + // Specific office requested - verify authorization + if (requestedOffice != null && !requestedOffice.isEmpty()) { + if (!allowedOffices.contains(requestedOffice)) { + return DSL.falseCondition(); + } + return officeField.eq(requestedOffice); + } + + // No specific office - filter to all allowed + return officeField.in(allowedOffices); +} +``` + +## Scenarios + +### Wildcard Access + +Users with administrative roles may have wildcard access to all offices: + +```json +{ + "constraints": { + "allowed_offices": ["*"] + } +} +``` + +The filter returns `noCondition()`, allowing access to data from any office. + +### Specific Office Request + +When a user requests data from a specific office (via query parameter): + +| User's allowed_offices | Requested office | Result | +|------------------------|------------------|--------| +| `["SWT", "SPK"]` | `SWT` | `office_id = 'SWT'` | +| `["SWT", "SPK"]` | `NWD` | `false` (denied) | +| `["*"]` | `NWD` | No condition (allowed) | + +### Multiple Office Access + +When no specific office is requested, the filter returns data from all allowed offices: + +```sql +WHERE office_id IN ('SWT', 'SPK', 'NWD') +``` + +### No Office Access + +If the `allowed_offices` array is empty, all access is denied: + +```java +if (allowedOffices.isEmpty()) { + return DSL.falseCondition(); +} +``` + +This results in a WHERE clause that always evaluates to false, returning no records. + +## Usage Example + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +// Get the office filter condition +Condition officeFilter = filterHelper.getOfficeFilter( + TIMESERIES.OFFICE_ID, // The office field in the table + requestedOffice // Office from query parameter (may be null) +); + +// Apply to query +SelectQuery query = dsl.selectFrom(TIMESERIES) + .where(officeFilter) + .getQuery(); +``` + +## Generated SQL Examples + +For a user with `allowed_offices: ["SWT", "SPK"]`: + +No specific office requested: +```sql +SELECT * FROM at_cwms_ts_id +WHERE office_id IN ('SWT', 'SPK') +``` + +Specific office requested (authorized): +```sql +SELECT * FROM at_cwms_ts_id +WHERE office_id = 'SWT' +``` + +Specific office requested (unauthorized): +```sql +SELECT * FROM at_cwms_ts_id +WHERE 1 = 0 +``` + diff --git a/docs/source/access-management/header-format/constraints.md b/docs/source/access-management/header-format/constraints.md new file mode 100644 index 0000000000..ea699af449 --- /dev/null +++ b/docs/source/access-management/header-format/constraints.md @@ -0,0 +1,228 @@ +# Constraints Schema + +The `constraints` object in the `x-cwms-auth-context` header defines data filtering rules that the Java API applies at the database query level. + +## Schema Reference + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `allowed_offices` | string[] | yes | Office IDs the user can access, or ["*"] for all | +| `embargo_rules` | object | no | Per-office embargo hours, null if no embargo | +| `embargo_exempt` | boolean | yes | Whether user bypasses embargo restrictions | +| `ts_group_embargo` | object | no | Per-time-series-group embargo hours | +| `time_window` | object | no | Restricts access to recent data only | +| `data_classification` | string[] | yes | Classification levels the user can access | + +## Field Definitions + +### allowed_offices + +Array of CWMS office identifiers that the user can access. The Java API filters query results to only include data from these offices. + +| Value | Meaning | +|-------|---------| +| `["SWT", "SPK"]` | User can access SWT and SPK office data | +| `["*"]` | User can access all offices (system admin, automated processor) | +| `[]` | No office access (effectively read-only public data) | + +### embargo_rules + +Object mapping office IDs to embargo periods in hours. Data newer than the embargo period is restricted. A `default` key provides the fallback for offices not explicitly listed. + +```json +{ + "SPK": 168, + "SWT": 72, + "default": 168 +} +``` + +The embargo period is measured from the current time backward. Data with timestamps within the embargo window is filtered out for non-exempt users. In the example above, SPK data less than 168 hours (7 days) old is embargoed. + +Set to `null` when no office-based embargo applies. + +### embargo_exempt + +Boolean flag indicating whether the user bypasses embargo restrictions entirely. Users with certain personas or roles are automatically exempt: + +| Exempt Personas | Exempt Roles | +|-----------------|--------------| +| data_manager | system_admin | +| water_manager | hec_employee | +| system_admin | data_manager | +| | water_manager | + +### ts_group_embargo + +Object mapping time series group IDs to embargo periods in hours. This provides granular embargo control at the time series group level, independent of office-based embargo. + +```json +{ + "Default": 0, + "Sensitive": 168, + "Operational": 24 +} +``` + +Set to `null` when no time-series-group-based embargo applies or when the user has no ts_privileges defined. + +### time_window + +Object restricting access to only recent data. Used for personas like dam_operator who should only see current operational data. + +| Field | Type | Description | +|-------|------|-------------| +| `restrict_hours` | number | Only data from the last N hours is accessible | + +```json +{ + "restrict_hours": 8 +} +``` + +Set to `null` when no time window restriction applies. + +### data_classification + +Array of data classification levels the user can access. Higher privilege users can access more restrictive classifications. + +| Level | Description | +|-------|-------------| +| `public` | Publicly available data | +| `internal` | Internal agency data | +| `restricted` | Restricted access data | +| `sensitive` | Sensitive operational data | + +Classification access by role: + +| User Type | Classifications | +|-----------|-----------------| +| Anonymous | public | +| Authenticated | public, internal | +| data_manager, water_manager | public, internal, restricted, sensitive | +| system_admin, hec_employee | public, internal, restricted, sensitive | + +## Complete Examples + +### Standard Authenticated User + +User with access to their assigned offices, subject to standard embargo rules. + +```json +{ + "allowed_offices": ["SWT"], + "embargo_rules": { + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "ts_group_embargo": null, + "time_window": null, + "data_classification": ["public", "internal"] +} +``` + +### Dam Operator + +Operator restricted to recent operational data from their office. + +```json +{ + "allowed_offices": ["SWT"], + "embargo_rules": null, + "embargo_exempt": true, + "ts_group_embargo": null, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] +} +``` + +### Water Manager + +Manager with full access to office data, exempt from embargo restrictions. + +```json +{ + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": true, + "ts_group_embargo": { + "Default": 0, + "Sensitive": 0 + }, + "time_window": null, + "data_classification": ["public", "internal", "restricted", "sensitive"] +} +``` + +### System Administrator + +Full system access with no restrictions. + +```json +{ + "allowed_offices": ["*"], + "embargo_rules": null, + "embargo_exempt": true, + "ts_group_embargo": null, + "time_window": null, + "data_classification": ["public", "internal", "restricted", "sensitive"] +} +``` + +### Anonymous User + +Public access only, subject to all embargo restrictions. + +```json +{ + "allowed_offices": [], + "embargo_rules": { + "default": 168 + }, + "embargo_exempt": false, + "ts_group_embargo": null, + "time_window": null, + "data_classification": ["public"] +} +``` + +### Partner with Time Series Group Access + +External partner with specific time series group privileges. + +```json +{ + "allowed_offices": ["SPK"], + "embargo_rules": { + "SPK": 168, + "default": 168 + }, + "embargo_exempt": false, + "ts_group_embargo": { + "Default": 72, + "Partner-Shared": 0 + }, + "time_window": null, + "data_classification": ["public", "internal"] +} +``` + +## Java API Implementation + +The `AuthorizationFilterHelper` class processes these constraints and generates JOOQ conditions: + +- `allowed_offices` generates `WHERE office_id IN (...)` conditions +- `embargo_rules` generates `WHERE data_timestamp < SYSDATE - (embargo_hours/24)` conditions +- `ts_group_embargo` generates per-group timestamp filters +- `time_window` generates `WHERE data_timestamp > SYSDATE - (restrict_hours/24)` conditions +- `data_classification` generates `WHERE classification IN (...)` conditions + +When multiple constraints apply, they are combined with AND logic. + diff --git a/docs/source/access-management/header-format/index.md b/docs/source/access-management/header-format/index.md new file mode 100644 index 0000000000..89c9fa041b --- /dev/null +++ b/docs/source/access-management/header-format/index.md @@ -0,0 +1,106 @@ +# x-cwms-auth-context Header + +The `x-cwms-auth-context` header carries authorization decisions and user context from the Authorization Proxy to the CWMS Data API. This header enables the Java API to enforce data filtering constraints at the database level without performing its own authorization logic. + +## Overview + +When a request passes through the Authorization Proxy, the proxy: + +1. Extracts the user identity from the JWT Bearer token +2. Queries the CWMS Data API for user context (roles, offices, privileges) +3. Sends the authorization request to OPA for policy evaluation +4. Constructs the `x-cwms-auth-context` header with the decision and constraints +5. Forwards the request to the CWMS Data API with the header attached + +The Java API receives this header and uses `AuthorizationFilterHelper` to apply the constraints as JOOQ `Condition` objects in database queries. + +## Header Structure + +The header value is a JSON-encoded object with the following top-level properties: + +| Property | Type | Description | +|----------|------|-------------| +| `policy` | object | OPA authorization decision with allow/deny result | +| `user` | object | User identity and attributes from CWMS database | +| `constraints` | object | Data filtering rules to apply at query time | +| `context` | object | Additional context passed from OPA decision | +| `timestamp` | string | ISO 8601 timestamp when the header was generated | + +## Complete Example + +```json +{ + "policy": { + "allow": true, + "decision_id": "proxy-1705734521234-abc123def" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "email": "m5hectest@example.com", + "roles": ["cwms_user", "ts_id_creator"], + "offices": ["SWT"], + "primary_office": "SWT", + "persona": "water_manager", + "ts_privileges": [ + { + "ts_group_code": 1, + "ts_group_id": "Default", + "privilege": "read-write", + "embargo_hours": 0 + } + ] + }, + "constraints": { + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "ts_group_embargo": { + "Default": 0, + "Sensitive": 168 + }, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] + }, + "context": {}, + "timestamp": "2025-01-20T12:15:21.234Z" +} +``` + +## Policy Object + +The `policy` object contains the OPA authorization decision. + +| Field | Type | Description | +|-------|------|-------------| +| `allow` | boolean | Whether the request is authorized | +| `decision_id` | string | Unique identifier for audit logging | + +## Java API Consumption + +The Java API parses this header in `AuthorizationFilterHelper.java` and generates JOOQ conditions for WHERE clauses. The helper extracts constraints and builds filter conditions that are applied to all relevant database queries. + +Key implementation points: + +- The header is only present on whitelisted endpoints that pass through OPA +- Non-whitelisted endpoints bypass the proxy and do not have this header +- The Java API must handle requests both with and without this header +- When present, the constraints in this header take precedence over any default filtering + +## Related Documentation + +- [User Context Schema](user-context.md) - User object field reference +- [Constraints Schema](constraints.md) - Data filtering constraints reference + +```{toctree} +:maxdepth: 2 + +user-context +constraints +``` diff --git a/docs/source/access-management/header-format/user-context.md b/docs/source/access-management/header-format/user-context.md new file mode 100644 index 0000000000..39f97907cc --- /dev/null +++ b/docs/source/access-management/header-format/user-context.md @@ -0,0 +1,121 @@ +# User Context Schema + +The `user` object in the `x-cwms-auth-context` header contains identity and attributes retrieved from the CWMS database and the user's JWT token. + +## Schema Reference + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | yes | Unique user identifier, typically matches username | +| `username` | string | yes | CWMS username from at_sec_cwms_users table | +| `email` | string | no | Email address from JWT claims | +| `roles` | string[] | yes | User groups from av_sec_users view | +| `offices` | string[] | yes | Office IDs the user belongs to | +| `primary_office` | string | no | Default office for the user | +| `persona` | string | no | User persona for role-based behavior | +| `region` | string | no | Geographic region assignment | +| `timezone` | string | no | User's preferred timezone | +| `shift_start` | number | no | Shift start hour (0-23) for time-based access | +| `shift_end` | number | no | Shift end hour (0-23) for time-based access | +| `authenticated` | boolean | no | Whether the user provided valid credentials | +| `auth_method` | string | no | Authentication method used (jwt, api_key, etc.) | +| `allowed_parameters` | string[] | no | Specific parameter IDs the user can access | +| `partnership_expiry` | string | no | ISO 8601 date when partnership access expires | +| `ts_privileges` | TsGroupPrivilege[] | no | Time series group access privileges | +| `attributes` | object | no | Additional custom attributes | + +## TsGroupPrivilege Schema + +The `ts_privileges` array contains per-group access settings. + +| Field | Type | Description | +|-------|------|-------------| +| `ts_group_code` | number | Numeric code for the time series group | +| `ts_group_id` | string | String identifier for the time series group | +| `privilege` | string | Access level: "read", "write", "read-write", or "none" | +| `embargo_hours` | number | Hours of embargo restriction for this group | + +## Persona Values + +The `persona` field controls behavior-specific access patterns. + +| Persona | Description | +|---------|-------------| +| `dam_operator` | Restricted to recent data (time_window applies) | +| `water_manager` | Full access to office data, embargo exempt | +| `data_manager` | Administrative access, embargo exempt | +| `automated_processor` | System access to all offices | +| `system_admin` | Full system access | + +## Example: Authenticated User + +```json +{ + "id": "m5hectest", + "username": "m5hectest", + "email": "m5hectest@example.com", + "roles": ["cwms_user", "ts_id_creator", "all_users"], + "offices": ["SWT"], + "primary_office": "SWT", + "persona": "water_manager", + "authenticated": true, + "auth_method": "jwt", + "ts_privileges": [ + { + "ts_group_code": 1, + "ts_group_id": "Default", + "privilege": "read-write", + "embargo_hours": 0 + }, + { + "ts_group_code": 2, + "ts_group_id": "Sensitive", + "privilege": "read", + "embargo_hours": 168 + } + ] +} +``` + +## Example: Anonymous User + +```json +{ + "id": "anonymous", + "username": "anonymous", + "email": "anonymous@example.com", + "roles": [], + "offices": [], + "authenticated": false +} +``` + +## Example: Dam Operator with Shift Hours + +```json +{ + "id": "operator123", + "username": "operator123", + "roles": ["cwms_user", "dam_operator"], + "offices": ["SWT"], + "primary_office": "SWT", + "persona": "dam_operator", + "timezone": "America/Chicago", + "shift_start": 6, + "shift_end": 18, + "authenticated": true +} +``` + +## Data Sources + +User context fields are populated from multiple sources: + +| Source | Fields | +|--------|--------| +| CWMS at_sec_cwms_users | username, offices, primary_office | +| CWMS av_sec_users | roles | +| JWT token claims | id (sub), email, preferred_username | +| OPA policy decision | persona (may be assigned by policy) | +| User configuration | timezone, shift_start, shift_end, region | + diff --git a/docs/source/access-management/index.md b/docs/source/access-management/index.md new file mode 100644 index 0000000000..66e89afc3d --- /dev/null +++ b/docs/source/access-management/index.md @@ -0,0 +1,62 @@ +# Access Management + +The CWMS Access Management system provides fine-grained authorization for the CWMS Data API. It uses a transparent proxy pattern combined with Open Policy Agent (OPA) to evaluate access policies before requests reach the backend API. + +## Overview + +The system implements a defense-in-depth security model with three distinct layers: + +| Layer | Component | Responsibility | +|-------|-----------|----------------| +| Authentication | Keycloak | JWT token issuance and validation | +| Authorization | OPA | Policy-based access control decisions | +| Data Filtering | CWMS Data API | Server-side constraint enforcement at SQL level | + +The authorization proxy sits between clients and the CWMS Data API, intercepting requests to evaluate policies and inject authorization context. The backend API applies filtering constraints at the database level based on this context, ensuring that users only see data they are permitted to access. + +## Key Principles + +The architecture follows several guiding principles: + +- All authorization decisions are made by OPA based on user context and configured policies +- The Java API does not make authorization decisions; it only applies constraints passed via headers +- User context is cached in Redis to reduce database load and improve response times +- Server-side filtering ensures data security regardless of client behavior + +## Documentation + +```{toctree} +:maxdepth: 2 + +architecture/index +configuration/index +filtering/index +header-format/index +integration/index +management/index +performance/index +policies/index +proxy-api/index +``` + +## Service Endpoints + +| Service | Port | Purpose | +|---------|------|---------| +| Authorization Proxy | 3001 | Request interception and policy evaluation | +| OPA | 8181 | Policy engine for authorization decisions | +| Redis | 6379 | User context caching | +| CWMS Data API | 7001 | Backend data API with SQL-level filtering | +| Keycloak | 8080 | Identity provider and JWT issuer | +| Management UI | 4200 | Web interface for policy management | + +## Technology Stack + +| Component | Technology | +|-----------|------------| +| Authorization Proxy | Node.js 24, TypeScript, Fastify | +| Policy Engine | OPA 0.68.0 | +| Cache | Redis 7.x | +| Data API | Java 11 | +| Database | Oracle 23c Free | +| Authentication | Keycloak 19.0.1 | diff --git a/docs/source/access-management/integration/authorization-context-helper.md b/docs/source/access-management/integration/authorization-context-helper.md new file mode 100644 index 0000000000..6d04c55239 --- /dev/null +++ b/docs/source/access-management/integration/authorization-context-helper.md @@ -0,0 +1,348 @@ +# AuthorizationContextHelper + +The `AuthorizationContextHelper` class parses the `x-cwms-auth-context` header and provides methods to access user information and constraints. + +**Package**: `cwms.cda.helpers` + +**Source**: `cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java` + +## Purpose + +This helper class serves as the bridge between the Authorization Proxy and the Java API. It: + +- Parses the JSON authorization context from the request header +- Extracts user identity information (id, username, email) +- Provides access to user roles and office assignments +- Exposes constraint values for filtering +- Respects the enabled/disabled configuration + +## Configuration + +The helper checks the `cwms.dataapi.access.management.enabled` property at class load time. When disabled, all methods return empty values regardless of header content. + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `cwms.dataapi.access.management.enabled` | boolean | false | Enable authorization header processing | + +The property can be set via environment variable or system property: + +```bash +# Environment variable +export cwms.dataapi.access.management.enabled=true + +# System property +java -Dcwms.dataapi.access.management.enabled=true ... +``` + +## Constructor + +```java +public AuthorizationContextHelper(Context ctx) +``` + +Creates a new helper instance by parsing the `x-cwms-auth-context` header from the Javalin context. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ctx` | `io.javalin.http.Context` | The Javalin request context | + +**Behavior**: +- If authorization is disabled, all internal maps are empty +- If header is missing or invalid, all internal maps are empty +- Invalid JSON is logged as a warning and treated as missing + +## Static Methods + +### isEnabled + +```java +public static boolean isEnabled() +``` + +Returns whether authorization mode is enabled. + +**Returns**: `true` if `cwms.dataapi.access.management.enabled` is set to `true` + +## User Context Methods + +### getUserId + +```java +public String getUserId() +``` + +Returns the user's unique identifier from the `user.id` field. + +**Returns**: User ID string, or `null` if not present + +### getUsername + +```java +public String getUsername() +``` + +Returns the user's username from the `user.username` field. + +**Returns**: Username string, or `null` if not present + +### getEmail + +```java +public String getEmail() +``` + +Returns the user's email address from the `user.email` field. + +**Returns**: Email string, or `null` if not present + +### getRoles + +```java +public List getRoles() +``` + +Returns the list of roles assigned to the user from the `user.roles` array. + +**Returns**: List of role names, or empty list if not present + +### getOffices + +```java +public List getOffices() +``` + +Returns the list of offices the user has access to from the `user.offices` array. + +**Returns**: List of office codes, or empty list if not present + +### getPrimaryOffice + +```java +public String getPrimaryOffice() +``` + +Returns the user's primary office from the `user.primary_office` field. + +**Returns**: Office code string, or `null` if not present + +### getPersona + +```java +public String getPersona() +``` + +Returns the user's active persona from the `user.persona` field. + +**Returns**: Persona name, or `null` if not present + +### getRegion + +```java +public String getRegion() +``` + +Returns the user's region from the `user.region` field. + +**Returns**: Region name, or `null` if not present + +## Constraint Methods + +### getAllowedOfficesConstraint + +```java +public String getAllowedOfficesConstraint() +``` + +Returns the allowed offices constraint value from `constraints.allowed_offices`. + +**Returns**: Constraint value string, or `null` if not present + +### isEmbargoExempt + +```java +public boolean isEmbargoExempt() +``` + +Returns whether the user is exempt from embargo rules based on `constraints.embargo_exempt`. + +**Returns**: `true` if user is embargo exempt, `false` otherwise + +### getTimezone + +```java +public String getTimezone() +``` + +Returns the user's timezone preference from `constraints.timezone`. + +**Returns**: Timezone string, or `null` if not present + +## Utility Methods + +### hasRole + +```java +public boolean hasRole(String role) +``` + +Checks if the user has a specific role. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `role` | `String` | The role name to check | + +**Returns**: `true` if the user has the specified role + +### hasOfficeAccess + +```java +public boolean hasOfficeAccess(String office) +``` + +Checks if the user has access to a specific office. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `office` | `String` | The office code to check | + +**Returns**: `true` if user has access, or if no authorization header is present + +### buildOfficeFilter + +```java +public String buildOfficeFilter() +``` + +Builds a comma-separated string of allowed offices for use in queries. + +**Returns**: +- `null` if no authorization header is present +- `null` if allowed offices constraint is `*` (all offices) +- Comma-separated office codes otherwise + +### isAuthorizationHeaderPresent + +```java +public boolean isAuthorizationHeaderPresent() +``` + +Checks if a valid authorization context header was present in the request. + +**Returns**: `true` if the header was present and successfully parsed + +### getFullContext + +```java +public Map getFullContext() +``` + +Returns an unmodifiable view of the complete parsed authorization context. + +**Returns**: Immutable map containing the full context, or empty map if not present + +## Usage Examples + +### Basic User Information + +```java +AuthorizationContextHelper auth = new AuthorizationContextHelper(ctx); + +if (auth.isAuthorizationHeaderPresent()) { + String username = auth.getUsername(); + String primaryOffice = auth.getPrimaryOffice(); + List roles = auth.getRoles(); + + logger.info("Request from {} at office {} with roles {}", + username, primaryOffice, roles); +} +``` + +### Role-Based Access Check + +```java +AuthorizationContextHelper auth = new AuthorizationContextHelper(ctx); + +if (!auth.hasRole("CWMS Users")) { + ctx.status(403).result("CWMS Users role required"); + return; +} +``` + +### Office Access Validation + +```java +AuthorizationContextHelper auth = new AuthorizationContextHelper(ctx); +String requestedOffice = ctx.queryParam("office"); + +if (!auth.hasOfficeAccess(requestedOffice)) { + ctx.status(403).result("Not authorized for office: " + requestedOffice); + return; +} +``` + +### Conditional Authorization + +```java +if (AuthorizationContextHelper.isEnabled()) { + AuthorizationContextHelper auth = new AuthorizationContextHelper(ctx); + // Apply authorization logic +} else { + // Bypass authorization +} +``` + +## Expected Header Format + +The helper expects the `x-cwms-auth-context` header to contain JSON in the following structure: + +```json +{ + "policy": { + "allow": true, + "decision_id": "proxy-12345" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "email": "m5hectest@usace.army.mil", + "roles": ["cwms_user", "ts_id_creator"], + "offices": ["SWT", "SPK"], + "primary_office": "SWT", + "persona": "operator", + "region": "SWD" + }, + "constraints": { + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"], + "timezone": "America/Chicago" + } +} +``` + +## Error Handling + +The helper logs warnings for parsing errors but does not throw exceptions. Invalid headers result in empty contexts: + +```java +AuthorizationContextHelper auth = new AuthorizationContextHelper(ctx); + +// Safe to call even if header was invalid +if (!auth.isAuthorizationHeaderPresent()) { + // Handle missing/invalid authorization +} +``` diff --git a/docs/source/access-management/integration/authorization-filter-helper.md b/docs/source/access-management/integration/authorization-filter-helper.md new file mode 100644 index 0000000000..df1953d451 --- /dev/null +++ b/docs/source/access-management/integration/authorization-filter-helper.md @@ -0,0 +1,392 @@ +# AuthorizationFilterHelper + +The `AuthorizationFilterHelper` class generates JOOQ `Condition` objects from the constraints in the `x-cwms-auth-context` header for use in database queries. + +**Package**: `cwms.cda.helpers` + +**Source**: `cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java` + +## Purpose + +This helper class translates authorization constraints into database query conditions. It: + +- Parses constraints from the authorization context header +- Generates JOOQ conditions for office-based filtering +- Applies embargo rules based on time restrictions +- Enforces time window limitations +- Filters by data classification levels + +## Constructors + +### From Javalin Context + +```java +public AuthorizationFilterHelper(io.javalin.http.Context ctx) +``` + +Creates a helper by extracting constraints from the `x-cwms-auth-context` header. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `ctx` | `io.javalin.http.Context` | The Javalin request context | + +**Behavior**: +- If authorization is disabled via `AuthorizationContextHelper.isEnabled()`, constraints are null +- If header is missing or invalid, constraints are null +- All filter methods return `DSL.noCondition()` when constraints are null + +### From JsonNode + +```java +public AuthorizationFilterHelper(JsonNode constraints) +``` + +Creates a helper from a pre-parsed constraints JSON node. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `constraints` | `com.fasterxml.jackson.databind.JsonNode` | Parsed constraints object | + +## Status Method + +### hasAuthorizationContext + +```java +public boolean hasAuthorizationContext() +``` + +Returns whether authorization constraints are present. + +**Returns**: `true` if constraints were successfully parsed + +## Filter Methods + +### getOfficeFilter + +```java +public Condition getOfficeFilter(Field officeField, String requestedOffice) +``` + +Generates a condition to filter results by allowed offices. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `officeField` | `Field` | The JOOQ field representing the office column | +| `requestedOffice` | `String` | The specific office requested (may be null) | + +**Returns**: JOOQ `Condition` object + +**Behavior**: + +| Scenario | Returned Condition | +|----------|-------------------| +| No constraints | `noCondition()` (no filtering) | +| No `allowed_offices` in constraints | `noCondition()` | +| `allowed_offices` contains `*` | `noCondition()` (all offices allowed) | +| `allowed_offices` is empty | `falseCondition()` (deny all) | +| `requestedOffice` not in allowed list | `falseCondition()` (deny) | +| `requestedOffice` in allowed list | `officeField.eq(requestedOffice)` | +| No `requestedOffice`, has allowed list | `officeField.in(allowedOffices)` | + +### getEmbargoFilter + +```java +public Condition getEmbargoFilter( + Field timestampField, + Field officeField, + String requestedOffice +) +``` + +Generates a condition to filter out embargoed data. Embargo rules restrict access to data newer than a specified number of hours. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `timestampField` | `Field` | The JOOQ field representing the timestamp column | +| `officeField` | `Field` | The JOOQ field representing the office column | +| `requestedOffice` | `String` | The specific office being queried | + +**Returns**: JOOQ `Condition` object + +**Behavior**: + +| Scenario | Returned Condition | +|----------|-------------------| +| No constraints | `noCondition()` | +| `embargo_exempt` is true | `noCondition()` | +| No `embargo_rules` | `noCondition()` | +| Office-specific rule exists | `timestampField.lessThan(cutoff)` | +| Default rule exists | `timestampField.lessThan(defaultCutoff)` | + +The cutoff timestamp is calculated as: `now - embargo_hours` + +For example, with a 168-hour embargo (7 days), users can only see data older than 7 days. + +### getTsGroupEmbargoFilter + +```java +public Condition getTsGroupEmbargoFilter( + Field timestampField, + String tsGroupId +) +``` + +Generates a condition to filter embargoed data based on time series group. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `timestampField` | `Field` | The JOOQ field representing the timestamp column | +| `tsGroupId` | `String` | The time series group identifier | + +**Returns**: JOOQ `Condition` object + +**Behavior**: + +| Scenario | Returned Condition | +|----------|-------------------| +| No constraints | `noCondition()` | +| `embargo_exempt` is true | `noCondition()` | +| No `ts_group_embargo` rules | `timestampField.lessThan(168-hour-cutoff)` | +| Group has 0 hours | `noCondition()` | +| Group-specific rule exists | `timestampField.lessThan(cutoff)` | +| Group not in rules | `timestampField.lessThan(168-hour-cutoff)` | + +### getTsGroupEmbargoHours + +```java +public int getTsGroupEmbargoHours(String tsGroupId) +``` + +Returns the embargo duration in hours for a specific time series group. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `tsGroupId` | `String` | The time series group identifier | + +**Returns**: Number of embargo hours (0 if no embargo applies) + +### getTimeWindowFilter + +```java +public Condition getTimeWindowFilter( + Field timestampField, + Timestamp userRequestedBeginTime +) +``` + +Generates a condition to restrict data to a recent time window. Unlike embargo rules (which hide recent data), time window rules limit how far back users can query. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `timestampField` | `Field` | The JOOQ field representing the timestamp column | +| `userRequestedBeginTime` | `Timestamp` | The begin time requested by the user (may be null) | + +**Returns**: JOOQ `Condition` object + +**Behavior**: + +| Scenario | Returned Condition | +|----------|-------------------| +| No constraints | `noCondition()` | +| No `time_window` | `noCondition()` | +| No `restrict_hours` | `noCondition()` | +| User time within window | `timestampField.greaterOrEqual(userRequestedBeginTime)` | +| User time before window | `timestampField.greaterOrEqual(cutoff)` | +| No user time specified | `timestampField.greaterOrEqual(cutoff)` | + +For example, with an 8-hour time window, a dam operator can only see data from the last 8 hours. + +### getClassificationFilter + +```java +public Condition getClassificationFilter(Field classificationField) +``` + +Generates a condition to filter by data classification level. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `classificationField` | `Field` | The JOOQ field representing the classification column | + +**Returns**: JOOQ `Condition` object + +**Behavior**: + +| Scenario | Returned Condition | +|----------|-------------------| +| No constraints | `noCondition()` | +| No `data_classification` | `noCondition()` | +| Empty classification list | `falseCondition()` (deny all) | +| Has allowed classifications | `classificationField.in(list).or(classificationField.isNull())` | + +Note: Records with null classification are included when classification filtering is active. + +### getAllFilters + +```java +public Condition getAllFilters( + Field officeField, + Field timestampField, + Field classificationField, + String requestedOffice, + Timestamp userRequestedBeginTime +) +``` + +Combines all filter types into a single condition using AND logic. + +**Parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `officeField` | `Field` | The JOOQ field for office | +| `timestampField` | `Field` | The JOOQ field for timestamp | +| `classificationField` | `Field` | The JOOQ field for classification (may be null) | +| `requestedOffice` | `String` | The specific office being queried | +| `userRequestedBeginTime` | `Timestamp` | The begin time requested by the user | + +**Returns**: Combined JOOQ `Condition` object + +**Behavior**: +- Returns `noCondition()` if no constraints are present +- Combines office, embargo, time window, and classification filters with AND +- Skips classification filter if `classificationField` is null + +## Usage Examples + +### Basic Query Filtering + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +SelectConditionStep query = dsl.selectFrom(TIMESERIES) + .where(filterHelper.getOfficeFilter(TIMESERIES.OFFICE_ID, requestedOffice)); +``` + +### Combined Filters + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +Condition authFilters = filterHelper.getAllFilters( + TIMESERIES.OFFICE_ID, + TIMESERIES.DATE_TIME, + TIMESERIES.CLASSIFICATION, + requestedOffice, + beginTime +); + +List results = dsl.selectFrom(TIMESERIES) + .where(authFilters) + .and(otherConditions) + .fetch(); +``` + +### Selective Filter Application + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +Condition baseCondition = LOCATIONS.OFFICE_ID.eq(office); + +if (filterHelper.hasAuthorizationContext()) { + Condition officeFilter = filterHelper.getOfficeFilter(LOCATIONS.OFFICE_ID, office); + baseCondition = baseCondition.and(officeFilter); +} + +List locations = dsl.selectFrom(LOCATIONS) + .where(baseCondition) + .fetch(); +``` + +### Embargo with Time Series Groups + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +String tsGroupId = determineTimeSeriesGroup(timeSeriesId); +Condition embargoFilter = filterHelper.getTsGroupEmbargoFilter( + TIMESERIES_VALUES.DATE_TIME, + tsGroupId +); + +List values = dsl.selectFrom(TIMESERIES_VALUES) + .where(TIMESERIES_VALUES.TS_CODE.eq(tsCode)) + .and(embargoFilter) + .fetch(); +``` + +### Checking Embargo Duration + +```java +AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + +int embargoHours = filterHelper.getTsGroupEmbargoHours("Reservoir-Levels"); + +if (embargoHours > 0) { + logger.info("Data embargoed for {} hours", embargoHours); +} +``` + +## Expected Constraints Format + +The helper expects the `constraints` object in the authorization context to have this structure: + +```json +{ + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "ts_group_embargo": { + "Reservoir-Levels": 24, + "Stream-Flow": 48 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `allowed_offices` | array | Office codes user can access, or `["*"]` for all | +| `embargo_rules` | object | Hours of recent data to hide per office | +| `embargo_rules.default` | number | Default embargo hours when office not specified | +| `ts_group_embargo` | object | Hours of recent data to hide per TS group | +| `embargo_exempt` | boolean | If true, embargo rules do not apply | +| `time_window.restrict_hours` | number | Only show data from last N hours | +| `data_classification` | array | Classification levels user can access | + +## JOOQ Condition Reference + +| Condition | Effect | +|-----------|--------| +| `DSL.noCondition()` | No filtering (all rows pass) | +| `DSL.falseCondition()` | Block all rows | +| `field.eq(value)` | Exact match | +| `field.in(list)` | Match any in list | +| `field.lessThan(value)` | Before timestamp (for embargo) | +| `field.greaterOrEqual(value)` | After timestamp (for time window) | +| `DSL.and(conditions...)` | All conditions must pass | +| `DSL.or(conditions...)` | Any condition must pass | diff --git a/docs/source/access-management/integration/index.md b/docs/source/access-management/integration/index.md new file mode 100644 index 0000000000..78062af06c --- /dev/null +++ b/docs/source/access-management/integration/index.md @@ -0,0 +1,169 @@ +# Java API Integration + +This section documents how the CWMS Data API integrates with the Authorization Proxy to enforce access control at the database level. + +## Overview + +The authorization architecture follows a clear separation of concerns: + +```mermaid +flowchart LR + subgraph Proxy + A[Authorization Proxy] + B[OPA Policy Engine] + end + subgraph JavaAPI[Java API] + C[AuthorizationContextHelper] + D[AuthorizationFilterHelper] + E[JOOQ Query Builder] + end + subgraph Database + F[Oracle DB] + end + + A --> B + B --> A + A -->|x-cwms-auth-context| C + C --> D + D --> E + E --> F +``` + +## Key Principle + +The Authorization Proxy makes authorization decisions and passes filtering constraints to the Java API via the `x-cwms-auth-context` header. The Java API does not make authorization decisions. Instead, it applies the constraints at the database query level to filter results. + +| Component | Responsibility | +|-----------|----------------| +| Authorization Proxy | Makes allow/deny decisions via OPA | +| OPA Policy Engine | Evaluates policies, determines constraints | +| AuthorizationContextHelper | Parses header, provides user context | +| AuthorizationFilterHelper | Generates JOOQ conditions from constraints | +| Database | Returns filtered results | + +## Helper Classes + +The Java API provides two helper classes for integration: + +### AuthorizationContextHelper + +Parses the `x-cwms-auth-context` header and provides access to user information and constraints. This class handles: + +- Extracting user identity (id, username, email) +- Retrieving user roles and office assignments +- Accessing constraint values +- Checking authorization header presence + +See [AuthorizationContextHelper](authorization-context-helper.md) for detailed documentation. + +### AuthorizationFilterHelper + +Generates JOOQ `Condition` objects from the constraints in the authorization context. This class handles: + +- Office-based filtering +- Embargo rules (time-based data restrictions) +- Time window restrictions +- Data classification filtering + +See [AuthorizationFilterHelper](authorization-filter-helper.md) for detailed documentation. + +## Enabling Authorization Mode + +Authorization integration is controlled by the `cwms.dataapi.access.management.enabled` configuration property. + +### Configuration Options + +| Method | Example | +|--------|---------| +| Environment Variable | `cwms.dataapi.access.management.enabled=true` | +| System Property | `-Dcwms.dataapi.access.management.enabled=true` | + +### Behavior by Mode + +**When Enabled (`true`)**: +- Authorization context header is parsed and validated +- Helper classes extract user context and constraints +- Filters are applied to database queries +- Requests without valid headers may be restricted + +**When Disabled (`false`, default)**: +- Authorization context header is ignored +- Helper classes return empty contexts +- No filters are applied to queries +- API behaves as if no authorization system exists + +```java +// Check if authorization mode is enabled +if (AuthorizationContextHelper.isEnabled()) { + // Apply authorization filters + AuthorizationContextHelper authContext = new AuthorizationContextHelper(ctx); + // ... +} +``` + +## Integration Flow + +```mermaid +sequenceDiagram + participant Client + participant Proxy as Authorization Proxy + participant OPA + participant API as Java API + participant DB as Oracle DB + + Client->>Proxy: Request with JWT + Proxy->>OPA: Evaluate policy + OPA-->>Proxy: Decision + constraints + Proxy->>API: Request + x-cwms-auth-context + API->>API: Parse header (ContextHelper) + API->>API: Build filters (FilterHelper) + API->>DB: Query with WHERE conditions + DB-->>API: Filtered results + API-->>Proxy: Response + Proxy-->>Client: Response +``` + +## Usage in Controllers + +Controllers integrate authorization filtering by: + +1. Creating helper instances from the Javalin context +2. Extracting relevant filter conditions +3. Applying conditions to JOOQ queries + +```java +public class TimeSeriesController extends BaseHandler { + + @Override + public void handle(Context ctx) { + AuthorizationContextHelper authContext = new AuthorizationContextHelper(ctx); + AuthorizationFilterHelper filterHelper = new AuthorizationFilterHelper(ctx); + + String requestedOffice = ctx.queryParam("office"); + + // Build query with authorization filters + Condition authFilters = filterHelper.getAllFilters( + TIMESERIES.OFFICE_ID, + TIMESERIES.DATE_TIME, + TIMESERIES.CLASSIFICATION, + requestedOffice, + userRequestedBeginTime + ); + + // Apply filters to query + List results = dsl.selectFrom(TIMESERIES) + .where(authFilters) + .fetch(); + } +} +``` + +## Contents + +```{toctree} +:maxdepth: 2 + +authorization-context-helper +authorization-filter-helper +testing +``` diff --git a/docs/source/access-management/integration/testing.md b/docs/source/access-management/integration/testing.md new file mode 100644 index 0000000000..2c30b29090 --- /dev/null +++ b/docs/source/access-management/integration/testing.md @@ -0,0 +1,117 @@ +# Testing + +The access management integration is tested through the existing CWMS Data API test infrastructure rather than isolated unit tests. This approach validates the authorization helpers work correctly within the actual request lifecycle. + +## Test Strategy + +Authorization filtering is verified through integration tests that exercise the full request path. The helpers are designed to be transparent - when disabled, they return no-op conditions that don't affect queries. + +| Approach | Coverage | +|----------|----------| +| Integration tests | Full request lifecycle with real database | +| Parameterized users | Different auth methods and permission levels | +| Existing endpoint tests | Verify filtering doesn't break current behavior | + +## Running Tests + +```bash +# Unit tests (fast, no database) +./gradlew test + +# Integration tests (requires database) +./gradlew integrationTests + +# Specific test class +./gradlew test --tests "*AuthorizationContextHelper*" +``` + +## Test User Fixtures + +The `UserSpecSource` class provides parameterized test users with different authentication methods: + +```java +@ParameterizedTest +@MethodSource("fixtures.users.UserSpecSource#userSpecsValidPrivs") +void testWithAuthorizedUser(String user, Consumer auth) { + given() + .spec(auth) + .when() + .get("/timeseries") + .then() + .statusCode(200); +} +``` + +Available user specs: + +| Method | Users Provided | +|--------|----------------| +| `userSpecsValidPrivs()` | Users with valid API keys and CWMS AAA sessions | +| `usersNoPrivs()` | Users without any permissions | +| `apiKeyUser()` | Single API key authenticated user | +| `cwmsAaaUser()` | Single CWMS AAA session user | + +## Testing with Access Management Disabled + +By default, tests run with access management disabled. To test authorization behavior: + +```bash +# Enable access management for tests +./gradlew integrationTests -Dcwms.dataapi.access.management.enabled=true +``` + +When disabled, the helpers return: +- `DSL.noCondition()` for all filters (no restrictions) +- Empty lists for roles and offices +- `false` for `isAuthorizationHeaderPresent()` + +## Writing Authorization-Aware Tests + +For tests that need to verify authorization behavior: + +```java +@Test +void testOfficeFiltering() { + // Set up mock authorization context + String authContext = """ + { + "user": {"offices": ["SWT"]}, + "constraints": {"allowed_offices": ["SWT"]} + } + """; + + given() + .header("x-cwms-auth-context", authContext) + .when() + .get("/timeseries?office=SWT") + .then() + .statusCode(200); +} +``` + +## Integration Test Base + +All integration tests extend `DataApiTestIT`, which handles: + +- Database connection setup +- Test data lifecycle management +- Automatic cleanup after tests +- Connection to TestContainers or bypass database + +```java +class MyAuthorizationTestIT extends DataApiTestIT { + @Test + void testAuthorizedAccess() { + // Test implementation + } +} +``` + +## Related Files + +| File | Purpose | +|------|---------| +| `fixtures/users/UserSpecSource.java` | Parameterized user providers | +| `fixtures/users/annotation/AuthType.java` | Auth type annotations | +| `api/auth/ApiKeyControllerTestIT.java` | API key authentication tests | +| `api/auth/OpenIdConnectTestIT.java` | Keycloak integration tests | diff --git a/docs/source/access-management/management/index.md b/docs/source/access-management/management/index.md new file mode 100644 index 0000000000..3af3c5982f --- /dev/null +++ b/docs/source/access-management/management/index.md @@ -0,0 +1,97 @@ +# Management Applications + +The CWMS Access Management system provides two management interfaces for administrators to view users, roles, and authorization policies: a web-based Management UI and a command-line Management CLI. + +## Overview + +Both applications connect to the Authorization Proxy management API to retrieve and display authorization data. They are read-only interfaces designed for monitoring and administration purposes. + +```mermaid +graph LR + UI[Management UI] + CLI[Management CLI] + API[Authorization Proxy] + OPA[OPA Server] + DB[(Oracle Database)] + + UI --> API + CLI --> API + API --> OPA + API --> DB +``` + +## Comparison + +| Feature | Management UI | Management CLI | +|---------|--------------|----------------| +| Interface | Web browser | Terminal | +| Authentication | Form-based login | Token-based login | +| User listing | Searchable table | Formatted table | +| User details | Detailed view | Show command | +| Role listing | Card view | Formatted table | +| Role details | Card expansion | Show command | +| Policy listing | Card view | Formatted table | +| Policy details | Card expansion | Show command | +| Session storage | Browser localStorage | Config file | +| Deployment | Container or static | Standalone binary | +| Best for | Interactive browsing | Scripting and automation | + +## When to Use Each Tool + +### Management UI + +Use the web interface when: + +- Browsing and searching through users interactively +- Presenting authorization data to non-technical stakeholders +- Working from a machine without CLI access +- Training new administrators + +### Management CLI + +Use the command-line interface when: + +- Automating administrative tasks with scripts +- Working in headless or SSH environments +- Integrating with CI/CD pipelines +- Performing quick lookups from the terminal + +## Common Capabilities + +Both applications support: + +- Secure authentication with JWT tokens +- View all registered users and their status +- View role definitions and descriptions +- View OPA authorization policies +- Automatic session management + +## Technology Stack + +| Component | Management UI | Management CLI | +|-----------|--------------|----------------| +| Runtime | Browser | Node.js 24+ | +| Language | TypeScript | TypeScript | +| Framework | React 18 | Commander | +| Bundler | Vite 6 | esbuild | +| State | Zustand, TanStack Query | Local config file | +| Styling | Tailwind CSS | Ink, Chalk | +| HTTP Client | Axios | Axios | +| Validation | Zod | Zod | + +## Port Assignments + +| Service | Default Port | +|---------|-------------| +| Management UI | 4200 | +| Management CLI | N/A (connects to proxy) | +| Authorization Proxy API | 3001 (proxy), 3002 (management) | + +## Detailed Documentation + +```{toctree} +:maxdepth: 1 + +management-ui +management-cli +``` diff --git a/docs/source/access-management/management/management-cli.md b/docs/source/access-management/management/management-cli.md new file mode 100644 index 0000000000..241325bc12 --- /dev/null +++ b/docs/source/access-management/management/management-cli.md @@ -0,0 +1,491 @@ +# Management CLI + +The CWMS Access Management CLI (cwms-admin) provides command-line access to manage users, roles, and authorization policies for the CWMS system. It offers an interactive terminal interface with formatted tables, colored output, and loading spinners. + +## Technology Stack + +| Component | Technology | Purpose | +|-----------|------------|---------| +| Runtime | Node.js 24+ | JavaScript execution environment | +| Language | TypeScript 5.6+ | Type-safe development | +| CLI Framework | Commander | Command parsing and help generation | +| Terminal UI | Ink | React-based terminal rendering | +| HTTP Client | Axios | API communication | +| Colors | Chalk | Terminal text styling | +| Spinners | Ora | Loading state indicators | +| Logging | Pino | Structured JSON logging | +| Validation | Zod | Schema validation | + +## Features + +- User management operations (list and view details) +- Role management operations (list and view details) +- Policy management operations (list and view details) +- Authentication with token persistence +- Formatted table output with box-drawing characters +- Colored status indicators +- Loading spinners during API calls +- Structured logging for debugging + +## Installation + +### NPM Installation (Recommended) + +```bash +npm install -g @usace/cwms-admin +``` + +### Download and Install + +Download the appropriate archive for your platform: + +| Platform | File | +|----------|------| +| macOS (Apple Silicon) | cwms-admin-v0.1.0-darwin-arm64.tar.gz | +| macOS (Intel) | cwms-admin-v0.1.0-darwin-x64.tar.gz | +| Linux | cwms-admin-v0.1.0-linux-x64.tar.gz | +| Any platform | cwms-admin-v0.1.0-portable.zip | + +Extract and run the installer: + +```bash +tar -xzf cwms-admin-v0.1.0-*.tar.gz +chmod +x install-from-archive.sh +./install-from-archive.sh +``` + +### Global Link from Source + +For development, link from the built distribution: + +```bash +cd dist/apps/cli/management-cli +npm link +``` + +## System Requirements + +| Requirement | Minimum | Recommended | +|-------------|---------|-------------| +| Node.js | 20.0.0 | 24.0.0+ | +| Disk Space | 50MB | 100MB | +| OS | macOS, Linux, Windows 10+ | macOS, Linux | +| Terminal | Unicode support | Color support | + +## Configuration + +### Configuration File + +The CLI stores configuration in `~/.cwms-admin/config.json`: + +```json +{ + "apiUrl": "http://localhost:3002", + "token": "your-auth-token", + "username": "admin" +} +``` + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| LOG_LEVEL | Logging level (debug, info, warn, error) | info | +| NODE_ENV | Environment (development, production) | production | +| MANAGEMENT_API_URL | Default API URL | http://localhost:3002 | +| KEYCLOAK_ADMIN_USER | Default admin username | admin | +| KEYCLOAK_ADMIN_PASSWORD | Default admin password | admin | + +## Command Reference + +### Global Options + +```bash +cwms-admin --version # Display version number +cwms-admin --help # Display help information +``` + +### Authentication Commands + +#### login + +Authenticate with the management API and store credentials. + +```bash +cwms-admin login [options] +``` + +| Option | Description | Default | +|--------|-------------|---------| +| -u, --username | Admin username | admin | +| -p, --password | Admin password | admin | +| -a, --api-url | Management API URL | http://localhost:3002 | + +Examples: + +```bash +cwms-admin login -u admin -p password +cwms-admin login -u admin -p password -a http://api.example.com:3002 +``` + +On success, the authentication token is saved to `~/.cwms-admin/config.json`. + +#### logout + +Clear stored credentials and log out. + +```bash +cwms-admin logout +``` + +### Users Commands + +#### users list + +Display all users in a formatted table. + +```bash +cwms-admin users list +``` + +Output columns: + +| Column | Description | +|--------|-------------| +| Username | User's login name | +| ID | Unique user identifier | +| Email | User's email address | +| Name | Full name (first + last) | +| Status | Enabled or Disabled (color coded) | + +#### users show + +Display detailed information for a specific user. + +```bash +cwms-admin users show +``` + +| Argument | Description | +|----------|-------------| +| id | User ID or username to display | + +Example: + +```bash +cwms-admin users show m5hectest +``` + +Output fields: + +| Field | Description | +|-------|-------------| +| Username | User's login name | +| ID | Unique identifier | +| Email | Email address (if set) | +| First Name | First name (if set) | +| Last Name | Last name (if set) | +| Status | Enabled or Disabled | + +### Roles Commands + +#### roles list + +Display all roles in a formatted table. + +```bash +cwms-admin roles list +``` + +Output columns: + +| Column | Description | +|--------|-------------| +| Name | Role name | +| ID | Unique role identifier | +| Description | Role description | + +#### roles show + +Display detailed information for a specific role. + +```bash +cwms-admin roles show +``` + +| Argument | Description | +|----------|-------------| +| id | Role ID to display | + +Example: + +```bash +cwms-admin roles show cwms_user +``` + +Output fields: + +| Field | Description | +|-------|-------------| +| Name | Role name | +| ID | Unique identifier | +| Description | Role description (if set) | + +### Policies Commands + +#### policies list + +Display all authorization policies in a formatted table. + +```bash +cwms-admin policies list +``` + +Output columns: + +| Column | Description | +|--------|-------------| +| Name | Policy name | +| ID | Unique policy identifier | +| Description | Policy description | + +#### policies show + +Display detailed information for a specific policy, including rule definitions. + +```bash +cwms-admin policies show +``` + +| Argument | Description | +|----------|-------------| +| id | Policy ID to display | + +Example: + +```bash +cwms-admin policies show office-restriction +``` + +Output fields: + +| Field | Description | +|-------|-------------| +| Name | Policy name | +| ID | Unique identifier | +| Description | Policy description | +| Rules | JSON-formatted policy rules | + +## Output Formats + +### Table Output + +List commands display data in formatted tables with box-drawing characters: + +``` +Found 3 users ++-----------+------+-----------------+------------+---------+ +| Username | ID | Email | Name | Status | ++-----------+------+-----------------+------------+---------+ +| m5hectest | 001 | m5@test.com | M5 Test | Enabled | +| l2hectest | 002 | l2@test.com | L2 Test | Enabled | +| l1hectest | 003 | - | L1 Test | Disabled| ++-----------+------+-----------------+------------+---------+ +``` + +### Detail Output + +Show commands display key-value pairs with aligned labels: + +``` +User Details +Username: m5hectest +ID: 001 +Email: m5@test.com +First Name: M5 +Last Name: Test +Status: Enabled +``` + +### Status Colors + +| Status | Color | +|--------|-------| +| Enabled | Green | +| Disabled | Red | +| Loading | Cyan | +| Warning | Yellow | +| Error | Red | + +## Exit Codes + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | General error (authentication failure, API error, validation error) | + +## Building from Source + +### Prerequisites + +- Node.js 24+ +- pnpm 10+ +- Access to the cwms-access-management monorepo + +### Build Steps + +```bash +cd cwms-access-management + +pnpm install + +pnpm nx build management-cli --configuration=production +``` + +Output location: `dist/apps/cli/management-cli/index.js` + +### Development Mode + +Run with hot reload during development: + +```bash +pnpm nx serve management-cli +``` + +Or using tsx directly: + +```bash +cd apps/cli/management-cli +pnpm dev +``` + +### Create Distribution Package + +```bash +./apps/cli/management-cli/scripts/build-executable.sh +``` + +Output in `./release/` directory: + +- Platform-specific tarballs (darwin-arm64, darwin-x64, linux-x64) +- Cross-platform portable ZIP archive + +## Troubleshooting + +### Command not found + +If you see "command not found: cwms-admin" after npm installation: + +```bash +export PATH="$PATH:$(npm bin -g)" +``` + +Add this line to `~/.bashrc` or `~/.zshrc` for persistence. + +### Permission denied + +For npm permission errors during global installation: + +```bash +npm config set prefix ~/.npm-global +export PATH=~/.npm-global/bin:$PATH +npm install -g @usace/cwms-admin +``` + +### Cannot connect to API + +Verify your configuration: + +```bash +cat ~/.cwms-admin/config.json +``` + +Re-authenticate with the correct API URL: + +```bash +cwms-admin login -u admin -p password -a http://correct-api-url:3002 +``` + +### Authentication required error + +If commands fail with "Not authenticated. Please run: cwms-admin login": + +```bash +cwms-admin login -u admin -p password +``` + +### Table rendering issues + +Ensure your terminal supports Unicode: + +```bash +echo $LANG +export LANG=en_US.UTF-8 +``` + +## API Integration + +The CLI communicates with the Management API service (default port 3002). All requests include the stored authentication token in the Authorization header. + +### Endpoints Used + +| Command | Method | Endpoint | +|---------|--------|----------| +| users list | GET | /users | +| users show | GET | /users/:id | +| roles list | GET | /roles | +| roles show | GET | /roles/:id | +| policies list | GET | /policies | +| policies show | GET | /policies/:id | +| login | POST | /login | + +### Request Timeout + +All API requests have a 10-second timeout. For slow network connections, ensure the Management API is accessible and responsive. + +## Project Structure + +``` +apps/cli/management-cli/ +├── src/ +│ ├── commands/ # Command implementations +│ │ ├── login.ts # Authentication commands +│ │ ├── users.tsx # User management commands +│ │ ├── roles.tsx # Role management commands +│ │ └── policies.tsx # Policy management commands +│ ├── ink/ +│ │ ├── components/ # Reusable UI components +│ │ │ ├── ink-table.tsx # Table component +│ │ │ └── status-message.tsx # Status display +│ │ ├── screens/ # Command output screens +│ │ │ ├── users-list.tsx +│ │ │ ├── user-details.tsx +│ │ │ ├── roles-list.tsx +│ │ │ ├── role-details.tsx +│ │ │ ├── policies-list.tsx +│ │ │ └── policy-details.tsx +│ │ └── render.ts # Ink rendering utilities +│ ├── services/ +│ │ └── api.service.ts # API client +│ ├── utils/ +│ │ ├── config.ts # Configuration management +│ │ ├── error.ts # Error handling utilities +│ │ ├── logger.ts # Pino logger setup +│ │ └── version.ts # Version utilities +│ └── index.ts # CLI entry point +├── scripts/ +│ ├── build-executable.sh # Distribution build script +│ ├── install-from-archive.sh # User installation script +│ └── prepare-dist.sh # Distribution preparation +├── docs/ +│ ├── installation.md # End-user installation guide +│ └── distribution.md # Build and distribution guide +├── package.json +└── tsconfig.json +``` + +## Related Documentation + +- [Management UI](management-ui.md) - Web-based management interface +- [Architecture Overview](../architecture/index.md) - System architecture +- [Proxy API Reference](../proxy-api/index.md) - Authorization proxy API documentation diff --git a/docs/source/access-management/management/management-ui.md b/docs/source/access-management/management/management-ui.md new file mode 100644 index 0000000000..d3f456d189 --- /dev/null +++ b/docs/source/access-management/management/management-ui.md @@ -0,0 +1,334 @@ +# Management UI + +The Management UI is a web-based interface for viewing CWMS authorization data including users, roles, and OPA policies. It provides a responsive, modern interface for administrators to browse and search authorization information. + +## Purpose + +The Management UI serves as the primary visual interface for: + +- Browsing registered users and their account status +- Viewing role definitions and their descriptions +- Inspecting OPA authorization policies +- Searching and filtering user lists + +This is a read-only interface. Administrative operations that modify data require the Management CLI or direct API access. + +## Technology Stack + +| Component | Technology | Version | +|-----------|------------|---------| +| UI Library | React | 18.3.1 | +| Build Tool | Vite | 6.x | +| Language | TypeScript | 5.6+ | +| Routing | React Router | 7.x | +| Data Fetching | TanStack Query | 5.x | +| State Management | Zustand | 5.x | +| Styling | Tailwind CSS | 3.4.x | +| HTTP Client | Axios | 1.x | +| Logging | Pino | 10.x | +| Testing | Vitest | 3.x | +| API Mocking | MSW | 2.x | + +## Features + +### Authentication + +- Form-based login with username and password +- JWT token storage in browser localStorage +- Automatic redirect to login on session expiration +- Logout functionality with token cleanup + +### Users Page + +- Paginated table of all registered users +- Real-time search filtering by username, email, or name +- User status indicators (active/inactive) +- Display of user ID, email, and full name + +### Roles Page + +- List view of all role definitions +- Role name and description display +- Role ID for reference + +### Policies Page + +- List view of OPA authorization policies +- Policy name and description display +- Policy ID for reference + +### Navigation + +- Responsive navigation bar +- Current user display with logout option +- Protected routes requiring authentication + +## Installation + +### Prerequisites + +- Node.js 24 or higher +- pnpm 10 or higher + +### From Monorepo + +Install dependencies from the monorepo root: + +```bash +pnpm install +``` + +## Configuration + +### Environment Variables + +Create a `.env` file in the application directory or set environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `VITE_API_URL` | Authorization Proxy management API URL | `http://localhost:3002` | +| `VITE_LOG_LEVEL` | Logging level (debug, info, warn, error) | `info` | +| `VITE_ENABLE_MSW` | Enable Mock Service Worker for development | `false` | + +### Example Configuration + +```bash +VITE_API_URL=http://localhost:3002 +VITE_LOG_LEVEL=info +VITE_ENABLE_MSW=false +``` + +## Running in Development + +Start the development server with hot reload: + +```bash +pnpm nx serve management-ui +``` + +The application will be available at [http://localhost:4200](http://localhost:4200). + +### Development Features + +- Hot module replacement for instant updates +- Source maps for debugging +- MSW integration for API mocking +- TypeScript type checking + +## Building for Production + +Build the optimized production bundle: + +```bash +pnpm nx build management-ui --configuration=production +``` + +Output location: `dist/apps/web/management-ui/` + +### Preview Production Build + +Test the production build locally: + +```bash +pnpm nx preview management-ui +``` + +Available at [http://localhost:4300](http://localhost:4300). + +## Docker Deployment + +### Building the Image + +Build the Docker image from the monorepo root: + +```bash +podman build \ + -t cwms-management-ui:local-dev \ + -f apps/web/management-ui/Dockerfile \ + --build-arg VITE_API_URL=http://localhost:3002 \ + . +``` + +The build argument `VITE_API_URL` is baked into the static assets at build time. + +### Running the Container + +```bash +podman run -d \ + --name management-ui \ + -p 4200:80 \ + cwms-management-ui:local-dev +``` + +### Docker Compose + +The application is included in the monorepo docker-compose configuration: + +```bash +podman compose -f docker-compose.podman.yml up -d management-ui +``` + +## Project Structure + +``` +src/ +├── components/ # Reusable UI components +│ └── ui/ # Base UI components (Button, Card, Input, Label) +├── contexts/ # React context providers +│ └── AuthContext.tsx # Authentication state management +├── pages/ # Page components for routing +│ ├── HomePage.tsx # Dashboard landing page +│ ├── LoginPage.tsx # Authentication page +│ ├── UsersPage.tsx # User listing and search +│ ├── RolesPage.tsx # Role listing +│ └── PoliciesPage.tsx # Policy listing +├── services/ # API clients +│ └── api.service.ts # Management API client +├── utils/ # Utility functions +│ ├── logger.ts # Pino logger configuration +│ └── utils.ts # General utilities +├── lib/ # Third-party integrations +│ └── utils.ts # Tailwind class utilities +├── App.tsx # Main application with routing +├── main.tsx # Application entry point +└── index.css # Global styles and Tailwind imports +``` + +## API Integration + +The UI connects to the Authorization Proxy management API: + +```mermaid +sequenceDiagram + participant Browser + participant UI + participant API + participant DB + + Browser->>UI: Login request + UI->>API: POST /login + API->>DB: Validate credentials + DB-->>API: User data + API-->>UI: JWT token + UI->>Browser: Store token + + Browser->>UI: View users + UI->>API: GET /users (with JWT) + API->>DB: Query users + DB-->>API: User list + API-->>UI: User data + UI->>Browser: Render table +``` + +### API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/login` | POST | Authenticate and receive JWT token | +| `/users` | GET | List all users | +| `/users/:id` | GET | Get user details | +| `/roles` | GET | List all roles | +| `/roles/:id` | GET | Get role details | +| `/policies` | GET | List all policies | +| `/policies/:id` | GET | Get policy details | + +## Available Scripts + +| Command | Description | +|---------|-------------| +| `pnpm nx serve management-ui` | Start development server | +| `pnpm nx build management-ui` | Build for production | +| `pnpm nx build management-ui --configuration=production` | Production build with optimizations | +| `pnpm nx preview management-ui` | Preview production build | +| `pnpm nx lint management-ui` | Run ESLint | +| `pnpm nx test management-ui` | Run tests | +| `pnpm nx test management-ui --coverage` | Run tests with coverage | +| `pnpm nx typecheck management-ui` | Run TypeScript type checking | + +## Authentication Flow + +1. User navigates to application +2. Protected routes check for existing token in localStorage +3. If no token, redirect to login page +4. User submits credentials +5. API returns JWT token on success +6. Token stored in localStorage and Zustand state +7. Subsequent API requests include token in Authorization header +8. On 401 response, token cleared and user redirected to login + +## Component Library + +The UI uses a custom component library built on Radix UI primitives: + +| Component | Description | +|-----------|-------------| +| Button | Action buttons with variants | +| Card | Container with header and content sections | +| Input | Form text input | +| Label | Form field labels | + +Components use Tailwind CSS for styling with the `class-variance-authority` library for variant management. + +## Testing + +Run the test suite: + +```bash +pnpm nx test management-ui +``` + +Run tests with the visual UI: + +```bash +pnpm nx test management-ui --ui +``` + +Generate coverage report: + +```bash +pnpm nx test management-ui --coverage +``` + +The test setup includes: + +- Vitest as the test runner +- MSW for API mocking +- React Testing Library for component tests + +## Troubleshooting + +### Application shows loading indefinitely + +Verify the API URL configuration matches the running Authorization Proxy: + +```bash +# Check if proxy is running +podman ps | grep authorizer-proxy + +# Verify API URL in .env +cat .env | grep VITE_API_URL +``` + +### Login fails with network error + +Ensure the Authorization Proxy management server is accessible: + +```bash +curl http://localhost:3002/health +``` + +### Build fails with TypeScript errors + +Run type checking to identify issues: + +```bash +pnpm nx typecheck management-ui +``` + +### Styles not loading in production + +Verify Tailwind CSS is properly configured and PostCSS is processing styles: + +```bash +pnpm nx build management-ui --verbose +``` diff --git a/docs/source/access-management/performance/benchmark-results.md b/docs/source/access-management/performance/benchmark-results.md new file mode 100644 index 0000000000..cf3c8a4a0e --- /dev/null +++ b/docs/source/access-management/performance/benchmark-results.md @@ -0,0 +1,146 @@ +# Benchmark Results + +This page documents benchmark results from testing the authorization proxy. These numbers provide a baseline for capacity planning, though actual production performance will vary based on hardware, network conditions, and workload patterns. + +## Test Environment + +The benchmarks were run on a local development machine with all services running in Podman containers. + +| Component | Details | +|-----------|---------| +| Machine | Apple M3 Max | +| Memory | 36GB RAM | +| OS | macOS Tahoe (Darwin 25.2.0) | +| Container Runtime | Podman 5.x | +| Node.js | 24.x (in container) | +| k6 Version | 1.5.0 | + +All services ran with default container resource limits. Production deployments with dedicated resources will see different numbers. + +## Request Latency + +The proxy adds minimal overhead to requests. Most of the time is spent in cache lookups and, when necessary, OPA policy evaluation. + +| Endpoint | Requests | Avg Latency | p95 Latency | Throughput | +|----------|----------|-------------|-------------|------------| +| `/health` | 6,695 | 1.2ms | 2ms | ~220 req/s | +| `/authorize` | 2,092 | 2.2ms | 3ms | ~70 req/s | +| `/cwms-data/timeseries` | 2,153 | 12ms | 15ms | ~70 req/s | +| Overall | 10,940 | 5.7ms | 12.4ms | ~177 req/s | + +The `/health` endpoint shows the baseline proxy overhead. The `/authorize` endpoint includes JWT decoding and OPA cache lookup. Authenticated proxy requests include the full authorization flow plus time spent waiting for the downstream API. + +## Cache Performance + +Caching dramatically reduces latency for repeated requests. The proxy uses two cache layers: + +| Cache Type | Hits | Misses | Hit Rate | Avg Lookup Time | +|------------|------|--------|----------|-----------------| +| User Context (Redis) | 4,235 | 10 | 99.76% | 0.23ms | +| OPA Decisions (In-Memory) | 4,238 | 7 | 99.84% | <0.1ms | + +The high hit rates reflect typical workloads where the same users make many requests. New users or cache expiration will cause misses that require backend lookups. + +### Cache Miss Impact + +When caches miss, latency increases significantly: + +| Operation | Cached | Uncached | +|-----------|--------|----------| +| User context lookup | <1ms | ~175ms | +| OPA decision | <0.1ms | 6-11ms | +| Total proxy overhead | 2-3ms | 180-200ms | + +The user context lookup dominates uncached latency because it requires an API call to fetch user profile information from the CWMS Data API. + +## OPA Policy Evaluation + +Policy evaluation happens only on cache misses. The evaluation time depends on policy complexity and the authorization decision: + +| Decision | Count | Avg Duration | p95 Duration | +|----------|-------|--------------|--------------| +| Allow | 3 | 6.4ms | ~10ms | +| Deny | 4 | 11ms | ~25ms | +| Total | 7 | 9ms | ~20ms | + +Deny decisions take longer because OPA often needs to evaluate more rules before determining that access should be blocked. + +## Latency Breakdown + +For an authenticated request, here is where time is spent: + +```mermaid +flowchart LR + subgraph Proxy["Authorization Proxy"] + JWT[JWT Decode
less than 1ms] + Redis[Redis Lookup
0.23ms hit / 1.2ms miss] + OPA[OPA Check
less than 0.1ms hit / 6-11ms miss] + end + subgraph Backend["Backend API"] + API[Request Processing
variable] + end + Client --> JWT --> Redis --> OPA --> API --> Client +``` + +Best case (cache hits): 2-3ms total proxy overhead +Worst case (cache misses): 180-200ms (dominated by user lookup API call) + +## Resource Utilization + +During the 30-second benchmark with 10 concurrent users: + +| Resource | Measurement | +|----------|-------------| +| CPU (proxy container) | 7.6 seconds total | +| Heap Size | 175MB | +| GC Events | 24 minor, 6 major | +| Peak Connections | 13 | + +The proxy maintains a steady memory footprint without significant growth over the test duration. + +## Throughput Under Load + +The stress test ramped from 5 to 50 virtual users over 60 seconds: + +| VU Count | Throughput | p95 Latency | Error Rate | +|----------|------------|-------------|------------| +| 5 | ~50 req/s | 8ms | 0% | +| 20 | ~150 req/s | 15ms | 0% | +| 50 | ~180 req/s | 45ms | 0% | + +Throughput scaled linearly up to about 30 VUs, then began to plateau as the proxy approached its capacity on the test hardware. + +## Comparison with Direct API Access + +To isolate the proxy overhead, we compared requests through the proxy versus direct API calls: + +| Path | Direct API | Through Proxy | Overhead | +|------|------------|---------------|----------| +| `/cwms-data/offices` | 8ms | 10ms | +2ms | +| `/cwms-data/timeseries` | 10ms | 13ms | +3ms | + +The 2-3ms overhead includes JWT decoding, cache lookups, and header injection. For most use cases, this is negligible compared to the security benefits. + +## Recommendations + +Based on these benchmarks: + +**For Production Deployment:** +- Deploy multiple proxy instances behind a load balancer for horizontal scaling +- Use Redis Cluster or Sentinel for cache high availability +- Monitor cache hit rates; rates below 90% may indicate configuration issues + +**For Performance Tuning:** +- Increase OPA decision cache TTL if policies change infrequently +- Consider pre-warming the cache for known high-traffic users +- Ensure Redis connection pooling is properly sized + +**Alerting Thresholds:** + +| Metric | Warning | Critical | +|--------|---------|----------| +| p95 latency | >100ms | >500ms | +| Cache hit rate | <90% | <80% | +| OPA evaluation p95 | >50ms | >100ms | +| Error rate | >1% | >5% | + diff --git a/docs/source/access-management/performance/index.md b/docs/source/access-management/performance/index.md new file mode 100644 index 0000000000..99b2918f82 --- /dev/null +++ b/docs/source/access-management/performance/index.md @@ -0,0 +1,59 @@ +# Performance Testing + +Understanding how the authorization proxy performs under load helps inform capacity planning and identify potential bottlenecks. This section covers the benchmarking tools, how to run performance tests, and what the results mean. + +## Why Performance Testing Matters + +The authorization proxy sits in the critical path for every request to the CWMS Data API. Even small latency increases can compound across thousands of requests. Performance testing helps answer questions like: + +- How much overhead does the proxy add to each request? +- At what point does the system start to degrade under load? +- Is the caching strategy effective? +- How does OPA policy evaluation scale? + +## Metrics Collected + +The proxy exposes Prometheus-compatible metrics at the `/metrics` endpoint. These provide insight into both real-time behavior and historical trends. + +| Metric Category | What It Measures | +|-----------------|------------------| +| Request latency | Time to process each request, broken down by endpoint | +| Cache performance | Hit and miss rates for both Redis and OPA caches | +| OPA evaluation | Time spent evaluating authorization policies | +| API calls | Latency when fetching user context from the backend | +| Connection tracking | Number of concurrent connections being handled | + +## Testing Approach + +Performance tests use [k6](https://k6.io/), a load testing tool that runs scenarios with simulated virtual users. The tests authenticate against Keycloak, make requests through the proxy, and measure response times. + +The test suite includes several scenarios that exercise different aspects of the system: + +| Scenario | Purpose | +|----------|---------| +| Public endpoints | Baseline measurement of proxy overhead | +| Authenticated with warm cache | Typical production behavior where users are already cached | +| Authenticated with cold cache | Worst-case latency when cache misses require backend calls | +| Direct authorization | Isolated measurement of policy evaluation | +| Stress test | System behavior under increasing load | + +## Quick Start + +For those wanting to run a quick benchmark locally: + +```bash +cd cwms-access-management/tools/benchmark + +# Run a 30-second quick test +k6 run quick-benchmark.js +``` + +Detailed instructions and result interpretation are covered in the following pages. + +```{toctree} +:maxdepth: 2 + +running-benchmarks +benchmark-results +metrics-reference +``` diff --git a/docs/source/access-management/performance/metrics-reference.md b/docs/source/access-management/performance/metrics-reference.md new file mode 100644 index 0000000000..728353c2fb --- /dev/null +++ b/docs/source/access-management/performance/metrics-reference.md @@ -0,0 +1,181 @@ +# Metrics Reference + +The authorization proxy exposes Prometheus-compatible metrics at the `/metrics` endpoint. These metrics provide visibility into request latency, cache effectiveness, and authorization decisions. + +## Accessing Metrics + +### Prometheus Format + +```bash +curl http://localhost:3001/metrics +``` + +Returns metrics in Prometheus text exposition format: + +``` +# HELP authorizer_proxy_http_requests_total Total HTTP requests +# TYPE authorizer_proxy_http_requests_total counter +authorizer_proxy_http_requests_total{method="GET",route="/health",status_code="200"} 6695 +``` + +### JSON Format + +```bash +curl http://localhost:3001/metrics/json | jq +``` + +Returns metrics as a JSON object for easier programmatic access: + +```json +{ + "http_requests_total": { + "GET /health 200": 6695, + "POST /authorize 200": 2092 + }, + "cache_hits_total": { + "user_context": 4235 + } +} +``` + +## Available Metrics + +### HTTP Request Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `authorizer_proxy_http_requests_total` | Counter | method, route, status_code | Total HTTP requests received | +| `authorizer_proxy_http_request_duration_seconds` | Histogram | method, route, status_code | Request latency distribution | +| `authorizer_proxy_active_connections` | Gauge | - | Current number of active connections | + +The request duration histogram uses default Prometheus buckets: 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 seconds. + +### Cache Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `authorizer_proxy_cache_hits_total` | Counter | cache_type | Successful cache lookups | +| `authorizer_proxy_cache_misses_total` | Counter | cache_type | Cache lookup failures | +| `authorizer_proxy_cache_operation_duration_seconds` | Histogram | operation, result | Time spent on cache operations | + +Cache types include: +- `user_context` - Redis cache for user profile data + +Cache operations include: +- `get` - Cache read operations +- `set` - Cache write operations + +### OPA Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `authorizer_proxy_opa_evaluations_total` | Counter | resource, action, decision | Policy evaluations by outcome | +| `authorizer_proxy_opa_evaluation_duration_seconds` | Histogram | resource, action, decision | Time spent evaluating policies | +| `authorizer_proxy_opa_cache_hits_total` | Counter | - | OPA decision cache hits | +| `authorizer_proxy_opa_cache_misses_total` | Counter | - | OPA decision cache misses | + +The decision label values are `allow` or `deny`. + +### API Call Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `authorizer_proxy_api_calls_total` | Counter | endpoint, status | Calls to downstream APIs | +| `authorizer_proxy_api_call_duration_seconds` | Histogram | endpoint, status | Downstream API latency | + +Endpoints tracked include: +- `/user/profile` - User context lookup from CWMS Data API + +### Authorization Decision Metrics + +| Metric | Type | Labels | Description | +|--------|------|--------|-------------| +| `authorizer_proxy_authorization_decisions_total` | Counter | result | Authorization outcomes (allow/deny/error) | + +## Querying Metrics + +### Cache Hit Rate + +Calculate the user context cache hit rate: + +```promql +sum(rate(authorizer_proxy_cache_hits_total{cache_type="user_context"}[5m])) / +(sum(rate(authorizer_proxy_cache_hits_total{cache_type="user_context"}[5m])) + + sum(rate(authorizer_proxy_cache_misses_total{cache_type="user_context"}[5m]))) +``` + +### Request Latency Percentiles + +Get the 95th percentile request latency: + +```promql +histogram_quantile(0.95, rate(authorizer_proxy_http_request_duration_seconds_bucket[5m])) +``` + +### OPA Evaluation Time + +Average OPA evaluation time by decision: + +```promql +rate(authorizer_proxy_opa_evaluation_duration_seconds_sum[5m]) / +rate(authorizer_proxy_opa_evaluation_duration_seconds_count[5m]) +``` + +### Error Rate + +Percentage of requests returning 5xx errors: + +```promql +sum(rate(authorizer_proxy_http_requests_total{status_code=~"5.."}[5m])) / +sum(rate(authorizer_proxy_http_requests_total[5m])) +``` + +## Grafana Dashboard + +For visualization, import these metrics into Grafana. A sample dashboard configuration might include: + +| Panel | Query | Visualization | +|-------|-------|---------------| +| Request Rate | `sum(rate(authorizer_proxy_http_requests_total[1m]))` | Time series | +| Latency p95 | `histogram_quantile(0.95, rate(authorizer_proxy_http_request_duration_seconds_bucket[1m]))` | Time series | +| Cache Hit Rate | See formula above | Gauge (0-100%) | +| Authorization Decisions | `sum by (result)(rate(authorizer_proxy_authorization_decisions_total[5m]))` | Pie chart | + +## Alerting Rules + +Example Prometheus alerting rules: + +```yaml +groups: + - name: authorizer-proxy + rules: + - alert: HighLatency + expr: histogram_quantile(0.95, rate(authorizer_proxy_http_request_duration_seconds_bucket[5m])) > 0.5 + for: 5m + labels: + severity: warning + annotations: + summary: "Authorization proxy p95 latency above 500ms" + + - alert: LowCacheHitRate + expr: | + sum(rate(authorizer_proxy_cache_hits_total[5m])) / + (sum(rate(authorizer_proxy_cache_hits_total[5m])) + + sum(rate(authorizer_proxy_cache_misses_total[5m]))) < 0.8 + for: 10m + labels: + severity: warning + annotations: + summary: "Cache hit rate below 80%" + + - alert: HighErrorRate + expr: | + sum(rate(authorizer_proxy_http_requests_total{status_code=~"5.."}[5m])) / + sum(rate(authorizer_proxy_http_requests_total[5m])) > 0.05 + for: 5m + labels: + severity: critical + annotations: + summary: "Error rate above 5%" +``` + diff --git a/docs/source/access-management/performance/running-benchmarks.md b/docs/source/access-management/performance/running-benchmarks.md new file mode 100644 index 0000000000..3b5f77bd8b --- /dev/null +++ b/docs/source/access-management/performance/running-benchmarks.md @@ -0,0 +1,169 @@ +# Running Benchmarks + +The authorization proxy includes a benchmark suite built with [k6](https://k6.io/). These tests simulate realistic traffic patterns to measure latency, throughput, and cache effectiveness. + +## Prerequisites + +You will need k6 installed on your machine. On macOS: + +```bash +brew install k6 +``` + +On other platforms, see the [k6 installation guide](https://grafana.com/docs/k6/latest/set-up/install-k6/). + +The benchmark assumes all services are running via Podman or Docker Compose: + +```bash +cd cwms-access-management +podman compose -f docker-compose.podman.yml up -d +``` + +Verify services are healthy: + +```bash +curl http://localhost:3001/health +curl http://localhost:8080/auth/realms/cwms +``` + +## Quick Benchmark + +For a quick sanity check, run the 30-second benchmark: + +```bash +cd cwms-access-management/tools/benchmark +k6 run quick-benchmark.js +``` + +This runs 10 virtual users making a mix of requests: +- Health checks (baseline latency) +- Authenticated requests through the proxy +- Direct authorization endpoint calls + +The quick benchmark authenticates against Keycloak using the test user credentials, so it exercises the full authorization flow. + +## Full Benchmark Suite + +The full suite runs five scenarios sequentially, taking approximately three minutes: + +```bash +cd cwms-access-management/tools/benchmark +./run-benchmark.sh +``` + +Or run it directly: + +```bash +k6 run scenarios.js +``` + +### Test Scenarios + +| Scenario | Duration | VUs | What It Tests | +|----------|----------|-----|---------------| +| Public Endpoints | 30s | 10 | Baseline proxy overhead on `/health` and `/ready` | +| Warm Cache | 30s | 20 | Repeated requests with same user (cache hits) | +| Cold Cache | 50 requests | 10 | Unique queries forcing cache misses | +| Authorization Endpoint | 30s | 15 | Direct `/authorize` API without proxying | +| Stress Test | 60s | 5-50 | Ramping load to find breaking point | + +## Environment Configuration + +The benchmarks read configuration from environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `PROXY_URL` | `http://localhost:3001` | Authorization proxy address | +| `KEYCLOAK_URL` | `http://localhost:8080` | Keycloak server address | +| `KEYCLOAK_REALM` | `cwms` | OAuth realm name | +| `KEYCLOAK_CLIENT_ID` | `cwms` | OAuth client ID | + +To run against a different environment: + +```bash +PROXY_URL=http://staging-proxy:3001 \ +KEYCLOAK_URL=http://staging-auth:8080 \ +k6 run scenarios.js +``` + +## Test Users + +The benchmarks use three test accounts with different permission levels: + +| User Key | Username | Office | Purpose | +|----------|----------|--------|---------| +| `damOperator` | `m5hectest` | SWT | Primary test user, CWMS Users role | +| `waterManager` | `l2hectest` | SPK | Tests cross-office access | +| `viewerUser` | `l1hectest` | SPL | Tests limited permissions | + +These users must exist in both Keycloak and the CWMS database. The Docker Compose setup configures them automatically. + +## Reading Results + +After running, k6 outputs a summary like: + +``` + checks.........................: 99.85% 10882 out 10898 + http_req_duration..............: avg=5.72ms p(95)=12.4ms + http_req_failed................: 0.00% 0 out of 10940 + authorization_latency..........: avg=3.1ms p(95)=8ms + cache_hit_rate.................: 98.50% +``` + +Key metrics to watch: + +| Metric | Good | Concerning | +|--------|------|------------| +| `http_req_duration` p95 | <100ms | >500ms | +| `http_req_failed` | 0% | >1% | +| `cache_hit_rate` | >95% | <80% | + +## Collecting Prometheus Metrics + +The proxy exposes metrics at `/metrics` that can be scraped during the benchmark: + +```bash +# Before running benchmark +curl http://localhost:3001/metrics > before.txt + +# Run benchmark +k6 run quick-benchmark.js + +# After running benchmark +curl http://localhost:3001/metrics > after.txt +``` + +For JSON format (easier to parse): + +```bash +curl http://localhost:3001/metrics/json | jq +``` + +## Troubleshooting + +### Token Acquisition Fails + +If you see `invalid_client` errors, verify the OAuth client exists in Keycloak: + +```bash +curl -X POST "http://localhost:8080/auth/realms/cwms/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=m5hectest&password=m5hectest&grant_type=password&client_id=cwms" +``` + +### Connection Refused + +Ensure all services are running: + +```bash +podman ps | grep -E 'authorizer-proxy|auth|opa|redis' +``` + +### High Error Rates + +Check proxy logs for details: + +```bash +podman logs -f authorizer-proxy +``` + diff --git a/docs/source/access-management/policies/helpers.md b/docs/source/access-management/policies/helpers.md new file mode 100644 index 0000000000..26e56d6c9f --- /dev/null +++ b/docs/source/access-management/policies/helpers.md @@ -0,0 +1,346 @@ +# Helper Functions + +Helper modules provide reusable functions for common authorization checks. These modules encapsulate complex logic for office hierarchy validation and time-based access rules. + +## Office Hierarchy Helper + +Location: `policies/helpers/offices.rego` + +The office helper manages hierarchical office relationships and determines if a user can access data from a specific office. + +### Office Data Structure + +Offices are organized in a hierarchy with metadata: + +```rego +offices := { + "HQ": { + "type": "headquarters", + "parent": null, + "region": "headquarters" + }, + "SPK": { + "type": "district", + "parent": "SPD", + "region": "south_pacific", + "timezone": "America/Los_Angeles" + }, + "SWT": { + "type": "district", + "parent": "SWD", + "region": "southwestern", + "timezone": "America/Chicago" + } +} +``` + +### Regional Organization + +Districts are grouped by region with their parent division: + +| Region | Division | Districts | +|--------|----------|-----------| +| South Pacific | SPD | SPK, SPN, SPL | +| Southwestern | SWD | SWT, SPA, GAL, FTW | +| Mississippi | MVD | MVR, MVS, MVP, MVM, MVN | + +```rego +regions := { + "southwestern": { + "division": "SWD", + "districts": ["SWT", "SPA", "GAL", "FTW"] + }, + "south_pacific": { + "division": "SPD", + "districts": ["SPK", "SPN", "SPL"] + }, + "mississippi": { + "division": "MVD", + "districts": ["MVR", "MVS", "MVP", "MVM", "MVN"] + } +} +``` + +### Office Access Rules + +The `user_can_access_office` function evaluates multiple conditions: + +```mermaid +graph TD + canAccess[user_can_access_office] --> inOffices{Office in user.offices?} + inOffices -->|Yes| allow[Allow] + inOffices -->|No| isDataMgr{User is data_manager with region?} + isDataMgr -->|Yes| inRegion{Office in user's region?} + inRegion -->|Yes| allow + inRegion -->|No| isProcessor + isDataMgr -->|No| isProcessor{User is automated_processor?} + isProcessor -->|Yes| allow + isProcessor -->|No| isSysAdmin{User has system_admin role?} + isSysAdmin -->|Yes| allow + isSysAdmin -->|No| isHecEmployee{User has hec_employee role?} + isHecEmployee -->|Yes| allow + isHecEmployee -->|No| deny[Deny] +``` + +### Access Rule Details + +**Direct Office Assignment**: + +```rego +user_can_access_office(user, office_id) if { + office_id in user.offices +} +``` + +User has explicit access to offices listed in their `offices` array. + +**Regional Access (Data Managers)**: + +```rego +user_can_access_office(user, office_id) if { + user.persona == "data_manager" + user.region != null + office_id in regions[user.region].districts +} +``` + +Data managers with a region assignment can access all districts within that region. + +**Cross-Regional Access (Automated Processors)**: + +```rego +user_can_access_office(user, office_id) if { + user.persona == "automated_processor" +} +``` + +Automated processors can access all offices for cross-regional data processing. + +**Privileged Role Access**: + +```rego +user_can_access_office(user, office_id) if { + "system_admin" in user.roles +} + +user_can_access_office(user, office_id) if { + "hec_employee" in user.roles +} +``` + +System admins and HEC employees have unrestricted office access. + +## Time Rules Helper + +Location: `policies/helpers/time_rules.rego` + +The time rules helper manages embargo periods, shift-based access, and modification windows. + +### Embargo System + +Embargo rules restrict access to recent data to prevent premature release. + +**Exempt Personas**: + +```rego +embargo_exempt_personas := ["data_manager", "water_manager", "system_admin", "hec_employee"] +``` + +| Persona | Embargo Exempt | +|---------|----------------| +| data_manager | Yes | +| water_manager | Yes | +| system_admin | Yes | +| hec_employee | Yes | +| All others | No | + +**Exemption Check**: + +```rego +user_embargo_exempt(user) if { + user.persona in embargo_exempt_personas +} +``` + +### TS Group Embargo + +Embargo periods are configured per time series group, allowing fine-grained control: + +```mermaid +graph TD + checkEmbargo[data_under_ts_group_embargo] --> isExempt{User embargo exempt?} + isExempt -->|Yes| notEmbargoed[Not Embargoed] + isExempt -->|No| hasTimestamp{Resource has timestamp?} + hasTimestamp -->|No| notEmbargoed + hasTimestamp -->|Yes| hasTsGroup{Resource has ts_group_id?} + hasTsGroup -->|No| notEmbargoed + hasTsGroup -->|Yes| getHours[Get embargo hours for ts_group] + getHours --> hoursPositive{Embargo hours > 0?} + hoursPositive -->|No| notEmbargoed + hoursPositive -->|Yes| inWindow{Data within embargo window?} + inWindow -->|No| notEmbargoed + inWindow -->|Yes| embargoed[Embargoed] +``` + +**TS Group Embargo Lookup**: + +```rego +get_ts_group_embargo_hours(user, ts_group_id) := hours if { + priv := user.ts_privileges[_] + priv.ts_group_id == ts_group_id + hours := priv.embargo_hours +} + +get_ts_group_embargo_hours(user, ts_group_id) := 168 if { + not ts_group_in_privileges(user, ts_group_id) +} +``` + +The function returns: +- Configured embargo hours if the TS group is in the user's privileges +- Default of 168 hours (7 days) if not found + +**Embargo Check**: + +```rego +data_under_ts_group_embargo(resource, user) if { + not user_embargo_exempt(user) + resource.timestamp_ns != null + resource.ts_group_id != null + embargo_hours := get_ts_group_embargo_hours(user, resource.ts_group_id) + embargo_hours > 0 + embargo_ns := embargo_hours * 60 * 60 * 1000000000 + time.now_ns() - resource.timestamp_ns < embargo_ns +} +``` + +### Legacy Office-Based Embargo + +A legacy system exists for backward compatibility with office-based embargo periods: + +| Office | Embargo Period | +|--------|----------------| +| SPK | 7 days | +| SWT | 3 days | +| DEFAULT | 7 days | + +```rego +embargo_periods := { + "SPK": 7 * 24 * 60 * 60 * 1000000000, + "SWT": 3 * 24 * 60 * 60 * 1000000000, + "DEFAULT": 7 * 24 * 60 * 60 * 1000000000 +} + +data_under_embargo(resource, user) if { + not user.persona in embargo_exempt_personas + resource.timestamp_ns != null + embargo_period := object.get(embargo_periods, resource.office, embargo_periods.DEFAULT) + time.now_ns() - resource.timestamp_ns < embargo_period +} +``` + +### Shift Hours + +Dam operators can only create or update data during their assigned shift hours. + +```rego +within_shift_hours(user) if { + user.persona == "dam_operator" + user.shift_start != null + user.shift_end != null + user.timezone != null + + current_hour := time.clock([time.now_ns(), user.timezone])[0] + current_hour >= user.shift_start + current_hour < user.shift_end +} + +within_shift_hours(user) if { + user.persona != "dam_operator" +} +``` + +**Evaluation Logic**: + +```mermaid +graph TD + checkShift[within_shift_hours] --> isDamOp{User is dam_operator?} + isDamOp -->|No| allowNotApplicable[Allow - not applicable] + isDamOp -->|Yes| hasStart{shift_start defined?} + hasStart -->|No| denyIncomplete[Deny - incomplete config] + hasStart -->|Yes| hasEnd{shift_end defined?} + hasEnd -->|No| denyIncomplete + hasEnd -->|Yes| hasTz{timezone defined?} + hasTz -->|No| denyIncomplete + hasTz -->|Yes| getCurrentHour[Get current hour in user timezone] + getCurrentHour --> afterStart{current_hour >= shift_start?} + afterStart -->|No| denyIncomplete + afterStart -->|Yes| beforeEnd{current_hour < shift_end?} + beforeEnd -->|No| denyIncomplete + beforeEnd -->|Yes| allowNotApplicable +``` + +**User Configuration Example**: + +```json +{ + "persona": "dam_operator", + "shift_start": 6, + "shift_end": 18, + "timezone": "America/Chicago" +} +``` + +This configuration allows operations between 6:00 AM and 6:00 PM Central Time. + +### Modification Window + +Dam operators can only update data within 24 hours of its creation: + +```rego +within_modification_window(resource, user) if { + user.persona == "dam_operator" + resource.created_ns != null + time.now_ns() - resource.created_ns < 24 * 60 * 60 * 1000000000 +} +``` + +| Constraint | Window | +|------------|--------| +| Creation to modification | 24 hours | +| Calculation | `current_time - created_time < 24h` | + +## Helper Usage in Personas + +Personas import and use helpers for authorization decisions: + +```rego +package cwms.personas.dam_operator + +import data.cwms.helpers.offices +import data.cwms.helpers.time_rules + +allow if { + "dam_operator" in input.user.roles + input.action == "read" + input.resource in ["timeseries", "measurements", "levels", "gates", "locations"] + offices.user_can_access_office(input.user, input.context.office_id) + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} +``` + +## Configuration Loading + +The office hierarchy and regions are currently defined statically in the policy. Future implementations may load this data dynamically from: + +- OPA Data API (`PUT /v1/data/cwms/offices`) +- CDA API queries +- Configuration files mounted at startup + +```rego +# In production, load data dynamically from: +# - OPA Data API: curl -X PUT http://localhost:8181/v1/data/cwms/offices -d @offices.json +# - Database Query: Query CDA API to get current office list +# - Configuration File: Load from config/offices.json at startup +``` + diff --git a/docs/source/access-management/policies/index.md b/docs/source/access-management/policies/index.md new file mode 100644 index 0000000000..bae8c04eb8 --- /dev/null +++ b/docs/source/access-management/policies/index.md @@ -0,0 +1,159 @@ +# Policy System Overview + +The CWMS Access Management system uses Open Policy Agent (OPA) to evaluate authorization decisions. This document describes the policy architecture, evaluation flow, and how policies interact with the authorization proxy. + +## Policy Architecture + +The policy system consists of three main components: + +1. **Main Orchestrator** (`cwms_authz.rego`) - Entry point that evaluates all persona policies +2. **Persona Policies** - Role-specific authorization rules in the `personas/` directory +3. **Helper Modules** - Reusable functions for office hierarchy and time-based rules + +```mermaid +graph TD + A[Authorization Request] --> B[cwms_authz.rego] + B --> C{Evaluate Personas} + C --> D[public] + C --> E[dam_operator] + C --> F[water_manager] + C --> G[data_manager] + C --> H[automated_collector] + C --> I[automated_processor] + C --> J[external_cooperator] + C --> K[viewer_users] + D --> L{Any Allow?} + E --> L + F --> L + G --> L + H --> L + I --> L + J --> L + K --> L + L -->|Yes| M[allow: true] + L -->|No| N[allow: false] +``` + +## Policy Evaluation + +### Input Structure + +OPA policies receive a standardized input structure from the authorization proxy: + +```json +{ + "user": { + "id": "m5hectest", + "roles": ["dam_operator", "cwms_user"], + "offices": ["SWT"], + "persona": "dam_operator", + "timezone": "America/Chicago", + "ts_privileges": [ + {"ts_group_id": "PRECIP", "embargo_hours": 72} + ] + }, + "action": "read", + "resource": "timeseries", + "context": { + "office_id": "SWT", + "classification": "public", + "data_source": "AUTOMATED", + "ts_group_id": "PRECIP", + "timestamp_ns": 1705708800000000000 + } +} +``` + +### Evaluation Order + +The main orchestrator (`cwms_authz.rego`) evaluates persona policies in sequence. Authorization succeeds if any persona policy returns `allow = true`: + +```rego +default allow := false + +allow if { public.allow } +allow if { dam_operator.allow } +allow if { water_manager.allow } +allow if { data_manager.allow } +allow if { automated_collector.allow } +allow if { automated_processor.allow } +allow if { external_cooperator.allow } +allow if { viewer_users.allow } +``` + +### Privileged Roles + +Two roles bypass persona-based evaluation entirely: + +| Role | Description | +|------|-------------| +| `system_admin` | Full system access, bypasses all persona checks | +| `hec_employee` | HEC staff access, bypasses all persona checks | + +```rego +allow if { "system_admin" in input.user.roles } +allow if { "hec_employee" in input.user.roles } +``` + +## Policy Decision Response + +OPA returns a decision that the authorization proxy uses to construct the `x-cwms-auth-context` header: + +```json +{ + "allow": true, + "decision_id": "opa-12345", + "constraints": { + "allowed_offices": ["SWT"], + "embargo_rules": {"SWT": 72, "default": 168}, + "embargo_exempt": false + } +} +``` + +## Helper Module Integration + +Persona policies import helper modules to evaluate complex conditions: + +```rego +import data.cwms.helpers.offices +import data.cwms.helpers.time_rules +``` + +Helpers provide: + +- **Office Validation** - Checks if user can access a specific office based on assignments, region, or role +- **Time Rules** - Evaluates embargo periods, shift hours, and modification windows + +See [helpers.md](helpers.md) for detailed documentation. + +## Policy File Organization + +``` +policies/ + cwms_authz.rego # Main orchestrator + personas/ + public.rego # Unauthenticated/public access + dam_operator.rego # Dam operators + water_manager.rego # Water managers + data_manager.rego # Data managers + automated_collector.rego # Automated data collection + automated_processor.rego # Automated data processing + external_cooperator.rego # External partners + viewer_users.rego # Read-only users + helpers/ + offices.rego # Office hierarchy and access + time_rules.rego # Embargo and time window rules +``` + +## Related Documentation + +- [Persona Policies](personas.md) - Detailed documentation for each persona +- [Helper Functions](helpers.md) - Office hierarchy and time rule helpers + +```{toctree} +:maxdepth: 2 + +personas +helpers +``` diff --git a/docs/source/access-management/policies/personas.md b/docs/source/access-management/policies/personas.md new file mode 100644 index 0000000000..e5f9cf174b --- /dev/null +++ b/docs/source/access-management/policies/personas.md @@ -0,0 +1,457 @@ +# Persona Policies + +Each persona represents a distinct user role with specific access patterns and constraints. The policy system evaluates the user's roles against these persona definitions to determine authorization. + +## Persona Summary + +| Persona | Role Identifier | Read | Create | Update | Delete | Embargo Exempt | +|---------|-----------------|------|--------|--------|--------|----------------| +| Public | (none required) | Limited | No | No | No | No | +| Dam Operator | `dam_operator` | Yes | Manual only | Manual only | No | No | +| Water Manager | `water_manager` | Yes | Yes | Yes | No | Yes | +| Data Manager | `data_manager` | Yes | Yes | Yes | Approved only | Yes | +| Automated Collector | `automated_collector` | No | Automated only | No | No | No | +| Automated Processor | `automated_processor` | Yes | Calculated only | Calculated only | No | No | +| External Cooperator | `external_cooperator` | Limited | Limited | No | No | No | +| Viewer Users | `Viewer Users` | Yes | No | No | No | No | + +## Public Access + +The public persona allows unauthenticated access to non-sensitive endpoints. + +### Allowed Operations + +**System Endpoints** (no authentication required): + +- `health`, `ready`, `metrics` + +**Reference Data** (read-only): + +- `offices`, `units`, `parameters`, `timezones` + +**Public Timeseries Data**: + +- Classification must be `public` +- Subject to TS group embargo rules + +**Public Locations**: + +- Classification must be `public` + +### Policy Logic + +```rego +package cwms.personas.public + +allow if { + input.resource in ["health", "ready", "metrics"] +} + +allow if { + input.action == "read" + input.resource in ["offices", "units", "parameters", "timezones"] +} + +allow if { + input.action == "read" + input.resource == "timeseries" + input.context.classification == "public" + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} + +allow if { + input.action == "read" + input.resource == "locations" + input.context.classification == "public" +} +``` + +## Dam Operator + +Dam operators monitor and manually enter operational data for their assigned facilities. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices only | +| Embargo | Subject to TS group embargo | +| Shift Hours | Create/update restricted to shift hours | +| Modification Window | Updates limited to 24 hours after creation | +| Data Source | Manual entries only | + +### Allowed Operations + +**Read Access**: + +- `timeseries`, `measurements`, `levels`, `gates`, `locations` +- Must have office access +- Subject to embargo rules + +**Create Access**: + +- `timeseries`, `measurements` +- Data source must be `MANUAL` +- Must be within shift hours +- Must have office access + +**Update Access**: + +- `timeseries`, `measurements` +- Data source must be `MANUAL` +- Must be within shift hours +- Resource must be within 24-hour modification window +- Must have office access + +### Policy Logic + +```rego +package cwms.personas.dam_operator + +allow if { + "dam_operator" in input.user.roles + input.action == "read" + input.resource in ["timeseries", "measurements", "levels", "gates", "locations"] + offices.user_can_access_office(input.user, input.context.office_id) + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} + +allow if { + "dam_operator" in input.user.roles + input.action == "create" + input.resource in ["timeseries", "measurements"] + input.context.data_source == "MANUAL" + offices.user_can_access_office(input.user, input.context.office_id) + time_rules.within_shift_hours(input.user) +} + +allow if { + "dam_operator" in input.user.roles + input.action == "update" + input.resource in ["timeseries", "measurements"] + input.context.data_source == "MANUAL" + offices.user_can_access_office(input.user, input.context.office_id) + time_rules.within_shift_hours(input.user) + time_rules.within_modification_window(input.context, input.user) +} +``` + +## Water Manager + +Water managers have broad access for hydrological analysis and forecasting within their assigned offices. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices only | +| Embargo | Exempt | +| Data Classification | All levels | + +### Allowed Operations + +**Read Access**: + +- `timeseries`, `locations`, `forecasts`, `models`, `scenarios`, `ratings`, `levels` +- Must have office access + +**Create/Update Access**: + +- `timeseries`, `locations`, `forecasts`, `models`, `scenarios` +- Must have office access + +### Policy Logic + +```rego +package cwms.personas.water_manager + +allow if { + "water_manager" in input.user.roles + input.action == "read" + input.resource in ["timeseries", "locations", "forecasts", "models", "scenarios", "ratings", "levels"] + offices.user_can_access_office(input.user, input.context.office_id) +} + +allow if { + "water_manager" in input.user.roles + input.action in ["create", "update"] + input.resource in ["timeseries", "locations", "forecasts", "models", "scenarios"] + offices.user_can_access_office(input.user, input.context.office_id) +} +``` + +## Data Manager + +Data managers have full data lifecycle control with approval workflows for destructive operations. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices and regional offices | +| Embargo | Exempt | +| Delete | Requires approval from different user | + +### Allowed Operations + +**Read/Create/Update Access**: + +- `timeseries`, `locations`, `catalogs`, `ratings`, `measurements`, `levels` +- Must have office access (direct or regional) + +**Delete Access**: + +- `timeseries`, `locations`, `catalogs`, `ratings` +- Must have office access +- Requires `approval_status == "approved"` +- Approver must be different from requester + +### Policy Logic + +```rego +package cwms.personas.data_manager + +allow if { + "data_manager" in input.user.roles + input.action in ["read", "create", "update"] + input.resource in ["timeseries", "locations", "catalogs", "ratings", "measurements", "levels"] + offices.user_can_access_office(input.user, input.context.office_id) +} + +allow if { + "data_manager" in input.user.roles + input.action == "delete" + input.resource in ["timeseries", "locations", "catalogs", "ratings"] + offices.user_can_access_office(input.user, input.context.office_id) + input.context.approval_status == "approved" + input.context.approver_id != input.user.id +} +``` + +## Automated Collector + +Service accounts for automated data collection systems. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices only | +| Authentication | API key required | +| Data Source | Automated only | + +### Allowed Operations + +**Create Access**: + +- `timeseries` only +- Data source must be `AUTOMATED` +- Authentication method must be `api_key` +- Must have office access + +### Policy Logic + +```rego +package cwms.personas.automated_collector + +allow if { + "automated_collector" in input.user.roles + input.action == "create" + input.resource == "timeseries" + input.context.data_source == "AUTOMATED" + offices.user_can_access_office(input.user, input.context.office_id) + input.user.auth_method == "api_key" +} +``` + +## Automated Processor + +Service accounts for data processing pipelines that read raw data and write calculated results. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | All offices (cross-regional processing) | +| Embargo | Subject to TS group embargo for reads | +| Data Source | Read: AUTOMATED/MANUAL, Write: CALCULATED only | + +### Allowed Operations + +**Read Access**: + +- `timeseries` only +- Data source must be `AUTOMATED` or `MANUAL` +- Subject to embargo rules +- No office restriction (cross-regional processing) + +**Create/Update Access**: + +- `timeseries` only +- Data source must be `CALCULATED` +- Must include `calculation_metadata` + +### Policy Logic + +```rego +package cwms.personas.automated_processor + +allow if { + "automated_processor" in input.user.roles + input.action == "read" + input.resource == "timeseries" + input.context.data_source in ["AUTOMATED", "MANUAL"] + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} + +allow if { + "automated_processor" in input.user.roles + input.action in ["create", "update"] + input.resource == "timeseries" + input.context.data_source == "CALCULATED" + input.context.calculation_metadata != null +} +``` + +## External Cooperator + +External partners with limited access governed by partnership agreements. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices only | +| Partnership | Must have active (non-expired) partnership | +| Parameters | Limited to allowed parameter types | +| Classification | Cannot access sensitive data | +| Embargo | Subject to TS group embargo | + +### Allowed Operations + +**Read Access**: + +- `timeseries` only +- Parameter must be in user's `allowed_parameters` list +- Classification cannot be `sensitive` +- Partnership must be active (not expired) +- Subject to embargo rules + +**Create Access**: + +- `timeseries` only +- Parameter must be in user's `allowed_parameters` list +- Must have office access +- Partnership must be active + +### Policy Logic + +```rego +package cwms.personas.external_cooperator + +partnership_active(user) if { + user.partnership_expiry_ns != null + user.partnership_expiry_ns > time.now_ns() +} + +allow if { + "external_cooperator" in input.user.roles + input.action == "read" + input.resource == "timeseries" + input.context.parameter in input.user.allowed_parameters + input.context.classification != "sensitive" + partnership_active(input.user) + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} + +allow if { + "external_cooperator" in input.user.roles + input.action == "create" + input.resource == "timeseries" + input.context.parameter in input.user.allowed_parameters + offices.user_can_access_office(input.user, input.context.office_id) + partnership_active(input.user) +} +``` + +## Viewer Users + +Read-only users with basic access to data within their assigned offices. + +### Constraints + +| Constraint | Value | +|------------|-------| +| Office Access | Assigned offices only | +| Embargo | Subject to TS group embargo | +| Actions | Read-only | + +### Allowed Operations + +**Reference Data** (read-only, no office restriction): + +- `offices`, `units`, `parameters`, `timezones` + +**Operational Data** (read-only): + +- `timeseries`, `locations`, `levels` +- Must have office access +- Subject to embargo rules + +### Policy Logic + +```rego +package cwms.personas.viewer_users + +allow if { + "Viewer Users" in input.user.roles + input.action == "read" + input.resource in ["offices", "units", "parameters", "timezones"] +} + +allow if { + "Viewer Users" in input.user.roles + input.action == "read" + input.resource in ["timeseries", "locations", "levels"] + offices.user_can_access_office(input.user, input.context.office_id) + not time_rules.data_under_ts_group_embargo(input.context, input.user) +} +``` + +## Policy Matching Logic + +When a request arrives, the main orchestrator evaluates each persona policy. The first matching rule grants access: + +```mermaid +graph TD + request[Request with User Roles] --> checkSysAdmin{Check system_admin} + checkSysAdmin -->|Yes| allow[Allow] + checkSysAdmin -->|No| checkHec{Check hec_employee} + checkHec -->|Yes| allow + checkHec -->|No| evalPersonas{Evaluate Persona Policies} + evalPersonas --> matchRoles[Match user.roles to persona] + matchRoles --> roleMatch{Role matches?} + roleMatch -->|No| nextPersona[Next persona] + nextPersona --> evalPersonas + roleMatch -->|Yes| checkAction{Check action} + checkAction -->|Invalid| nextPersona + checkAction -->|Valid| checkResource{Check resource} + checkResource -->|Invalid| nextPersona + checkResource -->|Valid| checkConstraints{Check constraints} + checkConstraints -->|Fail| nextPersona + checkConstraints -->|Pass| allow + evalPersonas -->|All exhausted| deny[Deny] +``` + +## Constraint Comparison + +| Constraint | Public | Dam Op | Water Mgr | Data Mgr | Auto Collect | Auto Process | Ext Coop | Viewer | +|------------|--------|--------|-----------|----------|--------------|--------------|----------|--------| +| Embargo Exempt | No | No | Yes | Yes | N/A | No | No | No | +| Office Restricted | No | Yes | Yes | Yes | Yes | No | Yes | Yes | +| Shift Hours | No | Yes | No | No | No | No | No | No | +| Mod Window | No | Yes | No | No | No | No | No | No | +| API Key Required | No | No | No | No | Yes | No | No | No | +| Partnership Required | No | No | No | No | No | No | Yes | No | +| Delete Approval | N/A | N/A | N/A | Yes | N/A | N/A | N/A | N/A | + diff --git a/docs/source/access-management/proxy-api/authorize-endpoint.md b/docs/source/access-management/proxy-api/authorize-endpoint.md new file mode 100644 index 0000000000..bd834519ca --- /dev/null +++ b/docs/source/access-management/proxy-api/authorize-endpoint.md @@ -0,0 +1,264 @@ +# POST /authorize Endpoint + +The `/authorize` endpoint provides authorization decisions for external services. It evaluates whether a user is allowed to perform a specific action on a resource based on OPA policies. + +## Request + +### URL + +``` +POST /authorize +``` + +### Headers + +| Header | Required | Description | +|--------|----------|-------------| +| `Content-Type` | Yes | Must be `application/json` | + +### Request Body + +```json +{ + "resource": "timeseries", + "action": "read", + "user": { + "id": "m5hectest", + "username": "m5hectest", + "roles": ["CWMS Users", "TS ID Creator"], + "offices": ["SWT"], + "persona": "operator", + "shift_start": 6, + "shift_end": 18, + "timezone": "America/Chicago" + }, + "context": { + "office_id": "SWT", + "data_source": "USGS" + } +} +``` + +### Body Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `resource` | string | Yes | Resource being accessed (e.g., `timeseries`, `locations`, `offices`) | +| `action` | string | Yes | Action being performed: `read`, `create`, `update`, `delete` | +| `user` | object | No | User context object (alternative to `jwt_token`) | +| `context` | object | No | Additional context for authorization decision | +| `jwt_token` | string | No | JWT token for user authentication (alternative to `user` object) | + +### User Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | User identifier | +| `username` | string | Username | +| `roles` | array | List of user roles | +| `offices` | array | List of offices the user belongs to | +| `persona` | string | Active persona (e.g., `operator`, `analyst`) | +| `shift_start` | number | Shift start hour (0-23) | +| `shift_end` | number | Shift end hour (0-23) | +| `timezone` | string | User timezone (IANA format) | + +### Context Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `office_id` | string | Office ID for the requested data | +| `data_source` | string | Data source identifier | +| `created_ns` | number | Creation timestamp in nanoseconds | +| `timestamp_ns` | number | Data timestamp in nanoseconds | + +Additional fields can be included as needed by the OPA policy. + +## Response + +### Success Response (200 OK) + +```json +{ + "decision": { + "allow": true, + "decision_id": "proxy-a1b2c3d4", + "reason": "User has read access to timeseries in office SWT" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "email": "m5hectest@example.com", + "roles": ["CWMS Users", "TS ID Creator"], + "offices": ["SWT"], + "primary_office": "SWT", + "persona": "operator" + }, + "constraints": { + "allowed_offices": ["SWT"], + "embargo_rules": { + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] + }, + "timestamp": "2024-01-15T10:30:00.000Z" +} +``` + +### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `decision.allow` | boolean | Whether the action is allowed | +| `decision.decision_id` | string | Unique identifier for this decision (for audit logging) | +| `decision.reason` | string | Human-readable explanation of the decision | +| `user` | object | Resolved user information | +| `constraints` | object | Data filtering constraints to apply | +| `timestamp` | string | ISO 8601 timestamp of the decision | + +### Constraints Object + +| Field | Type | Description | +|-------|------|-------------| +| `allowed_offices` | array | Offices the user can access, or `["*"]` for all | +| `embargo_rules` | object | Hours of embargo per office (data newer than X hours restricted) | +| `embargo_exempt` | boolean | Whether user is exempt from embargo rules | +| `time_window` | object | Time window restrictions for historical data | +| `data_classification` | array | Classification levels the user can access | + +### Error Responses + +#### 400 Bad Request + +Missing required fields: + +```json +{ + "error": "Bad Request", + "message": "resource and action are required fields" +} +``` + +Invalid action value: + +```json +{ + "error": "Bad Request", + "message": "action must be one of: read, create, update, delete" +} +``` + +#### 500 Internal Server Error + +```json +{ + "error": "Internal Server Error", + "message": "Authorization processing failed" +} +``` + +## Examples + +### Using curl with User Object + +```bash +curl -X POST http://localhost:3001/authorize \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "timeseries", + "action": "read", + "user": { + "id": "m5hectest", + "username": "m5hectest", + "roles": ["CWMS Users"], + "offices": ["SWT"] + }, + "context": { + "office_id": "SWT" + } + }' +``` + +### Using curl with JWT Token + +```bash +curl -X POST http://localhost:3001/authorize \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "timeseries", + "action": "create", + "jwt_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "context": { + "office_id": "SWT" + } + }' +``` + +### Checking Write Permission + +```bash +curl -X POST http://localhost:3001/authorize \ + -H "Content-Type: application/json" \ + -d '{ + "resource": "timeseries", + "action": "update", + "user": { + "id": "m5hectest", + "username": "m5hectest", + "roles": ["CWMS Users", "TS ID Creator"], + "offices": ["SWT"] + }, + "context": { + "office_id": "SWT", + "data_source": "manual" + } + }' +``` + +## Use Cases + +### Pre-flight Authorization Check + +External services can check authorization before attempting an operation: + +```mermaid +sequenceDiagram + participant Client + participant Proxy + participant OPA + + Client->>Proxy: POST /authorize + Proxy->>OPA: Evaluate policy + OPA-->>Proxy: Decision + constraints + Proxy-->>Client: Allow/Deny + constraints + + alt Allowed + Client->>Proxy: Actual API request + else Denied + Client->>Client: Handle denial + end +``` + +### Batch Authorization + +For batch operations, check authorization once and cache the constraints: + +```bash +# Get constraints for the session +CONSTRAINTS=$(curl -s -X POST http://localhost:3001/authorize \ + -H "Content-Type: application/json" \ + -d '{"resource": "timeseries", "action": "read", "jwt_token": "'$TOKEN'"}' \ + | jq -r '.constraints') + +# Use constraints for multiple requests +echo $CONSTRAINTS +``` + +## Related Documentation + +- [Data Filtering](../filtering/index.md) - How constraints affect data filtering +- [Authorization Context Header](../header-format/index.md) - Header format passed to downstream API diff --git a/docs/source/access-management/proxy-api/health-endpoints.md b/docs/source/access-management/proxy-api/health-endpoints.md new file mode 100644 index 0000000000..e7fb152ec0 --- /dev/null +++ b/docs/source/access-management/proxy-api/health-endpoints.md @@ -0,0 +1,257 @@ +# Health Endpoints + +The CWMS Authorization Proxy provides health and readiness endpoints for monitoring and container orchestration. + +## GET /health + +Basic health check endpoint that returns immediately if the service is running. + +### Request + +``` +GET /health +``` + +### Response + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T10:30:00.000Z", + "service": "authorizer-proxy" +} +``` + +### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `status` | string | Always `healthy` when the service is running | +| `timestamp` | string | ISO 8601 timestamp of the response | +| `service` | string | Service identifier (`authorizer-proxy`) | + +### Example + +```bash +curl http://localhost:3001/health +``` + +Response: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T10:30:00.000Z", + "service": "authorizer-proxy" +} +``` + +## GET /ready + +Readiness check that verifies the proxy can reach the downstream CWMS Data API. + +### Request + +``` +GET /ready +``` + +### Response (Ready) + +```json +{ + "status": "ready", + "downstream": "available", + "timestamp": "2024-01-15T10:30:00.000Z" +} +``` + +### Response (Not Ready) + +```json +{ + "status": "not-ready", + "downstream": "unavailable", + "timestamp": "2024-01-15T10:30:00.000Z" +} +``` + +### Response Fields + +| Field | Type | Description | +|-------|------|-------------| +| `status` | string | `ready` or `not-ready` | +| `downstream` | string | `available` or `unavailable` | +| `timestamp` | string | ISO 8601 timestamp of the response | + +### Example + +```bash +curl http://localhost:3001/ready +``` + +## Behavior + +### Health Check + +The `/health` endpoint: +- Returns immediately without external checks +- Always returns 200 OK if the server is accepting connections +- Lightweight check suitable for high-frequency polling + +### Readiness Check + +The `/ready` endpoint: +- Makes a HEAD request to the downstream CWMS Data API +- Uses a 5-second timeout for the downstream check +- Returns `ready` only if the downstream API responds successfully +- Returns `not-ready` if the downstream API is unreachable or returns an error + +```mermaid +flowchart TD + readyRequest[GET /ready] --> headRequest[HEAD request to CWMS API] + headRequest --> responseOk{Response OK?} + responseOk -->|Yes| returnReady[Return ready] + responseOk -->|No| returnNotReady[Return not-ready] + headRequest -->|Timeout/Error| returnNotReady +``` + +## Container Orchestration + +### Kubernetes + +Configure liveness and readiness probes: + +```yaml +apiVersion: v1 +kind: Pod +spec: + containers: + - name: authorizer-proxy + livenessProbe: + httpGet: + path: /health + port: 3001 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 3001 + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 3 +``` + +### Docker Compose + +Configure healthcheck in docker-compose: + +```yaml +services: + authorizer-proxy: + image: cwms-authorizer-proxy:local-dev + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s +``` + +### Podman + +Check container health manually: + +```bash +# Basic health check +podman exec authorizer-proxy curl -sf http://localhost:3001/health + +# Readiness check +podman exec authorizer-proxy curl -sf http://localhost:3001/ready +``` + +## Monitoring Scripts + +### Simple Health Check Script + +```bash +#!/bin/bash +response=$(curl -sf http://localhost:3001/health) +if [ $? -eq 0 ]; then + echo "Proxy is healthy" + exit 0 +else + echo "Proxy is not responding" + exit 1 +fi +``` + +### Readiness Check with Retry + +```bash +#!/bin/bash +max_attempts=30 +attempt=1 + +while [ $attempt -le $max_attempts ]; do + response=$(curl -sf http://localhost:3001/ready) + status=$(echo $response | jq -r '.status') + + if [ "$status" = "ready" ]; then + echo "Proxy is ready" + exit 0 + fi + + echo "Attempt $attempt: Proxy not ready, waiting..." + sleep 2 + attempt=$((attempt + 1)) +done + +echo "Proxy failed to become ready after $max_attempts attempts" +exit 1 +``` + +## Use Cases + +### Startup Sequencing + +Wait for the proxy to be ready before running integration tests: + +```bash +# Wait for proxy to be ready +until curl -sf http://localhost:3001/ready | jq -e '.status == "ready"' > /dev/null; do + echo "Waiting for proxy..." + sleep 2 +done + +echo "Proxy ready, running tests..." +npm test +``` + +### Load Balancer Health Checks + +Configure your load balancer to use the health endpoint: + +| Setting | Value | +|---------|-------| +| Health check path | `/health` | +| Health check interval | 30 seconds | +| Healthy threshold | 2 consecutive successes | +| Unhealthy threshold | 3 consecutive failures | +| Timeout | 5 seconds | + +### Alerting + +Monitor the ready endpoint for downstream issues: + +```bash +# Check every minute and alert if not ready +while true; do + status=$(curl -sf http://localhost:3001/ready | jq -r '.status') + if [ "$status" != "ready" ]; then + echo "ALERT: Proxy not ready - downstream may be unavailable" + fi + sleep 60 +done +``` diff --git a/docs/source/access-management/proxy-api/index.md b/docs/source/access-management/proxy-api/index.md new file mode 100644 index 0000000000..eed811b51c --- /dev/null +++ b/docs/source/access-management/proxy-api/index.md @@ -0,0 +1,119 @@ +# Proxy API Overview + +The CWMS Authorization Proxy exposes several API endpoints for authorization decisions, health monitoring, and transparent proxying of CWMS Data API requests. + +## Base URL + +The proxy listens on the configured `HOST` and `PORT` (default: `http://localhost:3001`). + +## Endpoint Categories + +### Authorization Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/authorize` | POST | Get authorization decision for a resource and action | + +### Health Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Basic health check | +| `/ready` | GET | Readiness check including downstream service availability | + +### Proxy Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/cwms-data/*` | ALL | Transparent proxy to CWMS Data API with authorization | + +## Authentication + +### Authorization Endpoint + +The `/authorize` endpoint accepts authentication via: + +1. **JWT Token** - Pass a JWT token in the request body (`jwt_token` field) +2. **User Object** - Pass user context directly in the request body (`user` field) + +### Proxy Endpoints + +Proxied requests to `/cwms-data/*` extract authentication from: + +1. **Authorization Header** - `Authorization: Bearer ` +2. **API Key Header** - `apikey: ` (for service-to-service communication) + +## Request Flow + +```mermaid +flowchart TD + A[Client Request] --> B{Endpoint Type} + B -->|/health, /ready| C[Return Status] + B -->|/authorize| D[Process Authorization] + B -->|/cwms-data/*| E{In Whitelist?} + E -->|Yes| F[OPA Policy Check] + E -->|No| G[Bypass Auth] + F --> H{Allowed?} + H -->|Yes| I[Proxy to CWMS API] + H -->|No| J[Return 403] + G --> I + D --> K[Return Decision] +``` + +## Response Format + +All API responses follow a consistent JSON structure: + +### Success Response + +```json +{ + "field": "value", + "timestamp": "2024-01-15T10:30:00.000Z" +} +``` + +### Error Response + +```json +{ + "error": "Error Type", + "message": "Human-readable error description" +} +``` + +## HTTP Status Codes + +| Code | Meaning | +|------|---------| +| 200 | Success | +| 400 | Bad Request - Invalid input | +| 401 | Unauthorized - Missing or invalid authentication | +| 403 | Forbidden - Authorization denied | +| 404 | Not Found - Endpoint does not exist | +| 500 | Internal Server Error - Unexpected error | +| 502 | Bad Gateway - Downstream service unavailable | + +## CORS Support + +The proxy enables CORS for all origins with the following settings: + +- Allowed methods: GET, POST, PUT, PATCH, DELETE, OPTIONS +- Credentials: Enabled +- All origins allowed (configurable for production) + +## OpenAPI Documentation + +The proxy includes Swagger/OpenAPI documentation available at: + +- Swagger UI: `http://localhost:3001/docs` +- OpenAPI JSON: `http://localhost:3001/docs/json` + +## Endpoint Documentation + +```{toctree} +:maxdepth: 1 + +authorize-endpoint +health-endpoints +``` diff --git a/docs/source/conf.py b/docs/source/conf.py index 029030e681..d24f162510 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -61,7 +61,7 @@ html_theme = "sphinx_rtd_theme" html_theme_options = { - "navigation_depth": 3, + "navigation_depth": 5, "collapse_navigation": False, "includehidden": False, #avoid pulling anchors/hidden items into the sidebar } @@ -69,4 +69,3 @@ # -- Options for EPUB output epub_show_urls = "footnote" - diff --git a/docs/source/data/index.rst b/docs/source/data/index.rst index f8a53e2f0e..21640bd2ad 100644 --- a/docs/source/data/index.rst +++ b/docs/source/data/index.rst @@ -1,5 +1,5 @@ Data Overview -================ +============= .. toctree:: :maxdepth: 1 diff --git a/docs/source/data/timeseries.rst b/docs/source/data/timeseries.rst index 2b4b23bb98..9f919ce71d 100644 --- a/docs/source/data/timeseries.rst +++ b/docs/source/data/timeseries.rst @@ -1,7 +1,31 @@ +.. _timeSeries_basics: + TimeSeries =========== -This page is coming soon if necessary or may be replaced by :ref:`timeseries-basics` +- What is a TimeSeries? + + `CWMS database - Time Series Definition `_ + + - A TimeSeries is a sequence of timestamped values measured or computed at a location for a specific + parameter (e.g., stage, flow). Each series may be recorded at a given interval (e.g., 15-min) and type + (e.g., observed, computed). Some series also have versions. + +- Data structure overview + + - Core Components: Location, Parameter, Type, Interval, Duration, Version + + `CWMS database - Component Definitions `_ + + +- Typical use cases + + - View most recent observation values + - Retrieve a historical range to chart or analyze + - Access a specific profile at a location/parameter + -Please check back later for updates and new content. +The time series endpoints allow you to retrieve and manage time series data stored in the CWMS database. +See the individual endpoint documentation for details on each available operation: +- :ref:`timeSeries-endpoints` diff --git a/docs/source/decisions/0001-api-versioning.rst b/docs/source/decisions/0001-api-versioning.rst new file mode 100644 index 0000000000..a0e5a10a6a --- /dev/null +++ b/docs/source/decisions/0001-api-versioning.rst @@ -0,0 +1,60 @@ +############################ +API Versioning and Reporting +############################ + +Summary +======= + +API should provide a reason version and list/matrix of capabilities for a given instance of CDA + +Opinions +======== + +Opinion 1 Calendar versioning +----------------------------- + +@MikeNeilson + +Summary: Calendar versioning is easier to support and automate with this API + +As the one whose has been making the "official" releases Semantic Version versioning has basically been useless. +We have been making so many feature additions that if I was doing it right we'd never have a minor version change between releases. +It's also caused me to, I think, release too slowly. + +While we can automate SemVer it is an additional step. + +With Calendar Versioning automation tools can just pick the current date when appropriately triggered, +perhaps by merged into a particular branch. + +@adamkorynta + + Summary: Calendar versioning more directly aligns with consumers needs + +Deprecation, desupport, and removal notices usually involve timelines rather than predicting x number of major/minor releases into the future. + +Given the endpoints themselves are already versioned, following semver becomes obtuse with only a real use-case of structural changes such as supported JDK changes, etc. + +Opinion 2 Users +--------------- + +Summary: It has been asked more than once that a version be provided + +Having a version allows client to better respond to what's available instead of failing in obtuse ways. + + +Decision Status +=============== + +accepted + +1. Provide endpoint to retrieve current API version. +2. Likely include capability list or matrix. + +References +========== + + +Related decisions +================= + +- data-versioning diff --git a/docs/source/decisions/0002-data-versioning.rst b/docs/source/decisions/0002-data-versioning.rst new file mode 100644 index 0000000000..55e69aafbe --- /dev/null +++ b/docs/source/decisions/0002-data-versioning.rst @@ -0,0 +1,90 @@ +################################## +Data Types use Calendar Versioning +################################## + +Summary +======= + +Instead of versioning the entire API, we version the data types if appropriate. +This versioning only applies to simple non-breaking changes and it primarily presented as a hint in the returned +data. + +Opinions +======== + +Opinion 1 +--------- + +Summary: If a given end point can have additional, or modified, elements in the response, add a data version. + + +@MikeNeilson + +By versioning the data, and using the Content-Type and Accept headers and the full features of MIME types we appropriately +separate the concern of "what data we are retrieving/storing" from the presentation of data. + +e.g /timeseries/Alder Springs.Temp-Air.Inst.15Minutes.0.GOES-raw?begin=PT0&end=PT-1D&units=C, granted with a reasonable + exception to the units, defines *what* we want. + +The header, Accept, informs the API what format, or formats, we are willing to accept the data in. + +Opinion 2 +--------- + +Summary: Remove data versioning in all endpoints. + +@adamkorynta + +The accept/content-type headers have provided sufficient confusion to downstream clients and deviate from industry standards. +I have yet to see an endpoint where this versioning solves the proposed problem of needing to different shapes of data (other than json vs xml). +The concept was introduced to solve the straight-to-db queries, which did not have any OpenAPI documentation moving to in-app DTO's which now have documentation. When moving away from the straight-to-db queries, we needed to thoroughly expand queries parameters and make other backwards-incompatible changes unrelated to the data shape. Given that context in hindsight a path version would have worked better. + +The closest we've gotten to needing new shapes on the same endpoint is the data-entry date on TimeSeries, but this was +more appropriately solved via query parameter and data arrays. I think if we had really wanted to be a stickler on the +"what format" we could have easily added another endpoint path instead. Even if/when we add text annotations, +using a content type is obtuse given the lack of discoverability as we would then need `application/json`, +`application/json+data-entry+text-annotations`, `application/json+text-annotations`, `application/json+data-entry` +which seems like just another type of bloat that is more hidden from clients. + +Decision Status +--------------- + +`rejected` - the descriptions in this proposal are awkward and it is not clear how to fix them. Additionally as we have +decided to adopt path based versioning and we've made `application/json` or `application/xml` default to the latest desired +the requirement is now moot. + +Data, by content-types, are versioned. In the past there was some severe confusion on this part and it was treated as anything +new was "version=2" in the content-type. To allow this design but reduce confusion going forward + +1. The initial content-type of a data set *SHALL* be be the plain content-type and *SHOULD* include an additional expanded content-type +3. *IF* is it not the first version of this data, additional information will be set in the content-type as + appropriate to the to the data. (e.g. `application/json+` or `application/json;`) + 1. It will be discussed and announced when it becomes the new default data, if that decision is made. +5. Downstream systems *SHOULD* use the specific version regardless of when implemented, and this behavior should be well documented. +6. If a given data set includes definitions of its shape within the type there should be sufficient documentation for downstream + developers to properly account for any changes over time. (See our TimeSeries type and discussions within #927). + +[comment:] <> (Status: request for comments | proposed | accepted | rejected | deprecated | superseded) + +References +========== + +1. https://www.youtube.com/watch?v=jmoxGJ_sLgU +2. https://newsletter.systemdesign.one/p/api-versioning +3. https://www.speakeasy.com/api-design/versioning + + +Notes +===== + +The initial idea in CDA was that the first version of any data type was, we'll just stick with JSON for each of message, +"application/json;version=1" with "application/json" being the alias to the latest format version. However, this was not +correctly communicated and several brand new data transfer objects were created as ";version=2" under the impression that +this was the version for the new system. Attempting to use a simple number of this has clearly caused confusion in general. + +We also failed to create the initial alias system which caused even more confusion when users attempted to test things +directly in a browser instead of the provided swagger-ui. + + +Various practical concerns and common usage have also made doing this "pedantically correct" impossible to manage. The above +should be a reasonable compromise. \ No newline at end of file diff --git a/docs/source/decisions/0003-searchability-and-catalogs.rst b/docs/source/decisions/0003-searchability-and-catalogs.rst new file mode 100644 index 0000000000..679f637c15 --- /dev/null +++ b/docs/source/decisions/0003-searchability-and-catalogs.rst @@ -0,0 +1,80 @@ +################################# +Data should be readily searchable +################################# + +Summary +======= + +It's not just a good idea, it's technically the law, see reference 1 and 2. While CDA currently expose a fair amount +of information to search it's never entirely clear. We *MUST* adopt a standard method of searchability. + +Additional Information +---------------------- + +Catalog - a complete list of items, for examples of things that people can look at or buy [3] + +By having a well defined structure of information users can more easily discover what they are looking for. While the +Swagger-UI, if used, presented all of the types of data that can be found. The `catalog` for each type should present +a clear way to find the available data of each type. + +The catalog of each data set would include only metadata associated with each data type. For example a time series +catalog would include support to discover primary timeseries names, aliases, extents, and the like but not actual time +series data. + +However, for data that are primarily metadata, such as Locations, they would be one and the same. + +Opinions +======== + +Opinion 1 +--------- + +Summary: Each data type should support it's own /catalog end point. + +@MikeNeilson + +The original CDA has the say, `/timeseries` end point provide a catalog if no data is set. I created a /catalog end point +to attempt to consolidate search query parameters. For TimeSeries and Locations this works reasonably well since there +is parity between the concepts. + +However, if we tried to add ratings into the mix, the list of query parameters grows, and it would rather difficult to +document which is for what or what changes for each. + +To make 'catalog' operations clear, we should create /catalog for each data type that provide for discoverability of that data. + +Opinion 2 +--------- + +Summary: Each datatype should exit under a "/catalog" + +@MikeNeilson + +If it makes sense to group all catalogs under catalog, perhaps for grouping in the SWAGGER-UI, making each catalog it's own +path under `/catalog` instead of the current path parameter is a better approach. + +We would maintain the grouping, but each catalog can have its appropriate search criteria. the `catalog/` could +just redirect. + +Opinion 3 +--------- + +Summary: Swagger-UI allows grouping under multiple tasks + +@Mike Neilson on behalf of @adamkorynta + +We can group the available catalogs into multiple Swagger-UI blocks while maintaining a `/catalog` + +Decision Status +=============== + +proposed - requires additional discussion and likely some review after the first path based version is adopted. +Document is left in proposed state to indicate additional ideas should be presented over time and as work is done. + +[comment:] <> (Status: request for comments | proposed | accepted | rejected | deprecated | superseded) + +References +========== + +1. https://www.congress.gov/bill/115th-congress/house-bill/1770 +2. https://www.cio.gov/handbook/it-laws/ogda/ +3. https://www.oxfordlearnersdictionaries.com/us/definition/english/catalogue_1 \ No newline at end of file diff --git a/docs/source/decisions/0004-versioning.rst b/docs/source/decisions/0004-versioning.rst new file mode 100644 index 0000000000..a443c367f5 --- /dev/null +++ b/docs/source/decisions/0004-versioning.rst @@ -0,0 +1,84 @@ +######################## +CWMS Data Api Versioning +######################## + + +Summary +======= + +Maintaining backwards compatibility while improving future difficulty has proven sufficiently difficulty that change +is required. + +The API as a whole will retain the calendar based versioning for formal releases. +Data *SHOULD* be versioned, if appropriate/needed, with otherwise backwards compatible changes to query parameters. +Endpoints will be placed under a new "api version" path parameter for backwards incompatible or confusing parameter changes. + +e.g. + +`https://host/cwms-data/locations` + +can become + +`https://host/cwms-data/v2/locations` + +As additional endpoints require such a change they should be added to an existing increased version. For each +version all required verbs *SHALL* be implemented. e.g. the new version is a complete unit of operation. + +Example: + +given above and a v1 `timeseries` and yet another `locations` improvements + +.. code-block:: bash + + # new time series becomes + cwms-data/v2/timeseries + # the new location becomes + cwms-data/v3/locations + +.. NOTE:: + + Or is that confusing and we should just allows add a new endpoint to the highest endpoint version? + +At Current time the "root" URL will be considered V1, and redirect to v1 urls. +After X years the root URLs will redirect to the latest version. + +e.g. + + .. code-block:: bash + # now + curl "https://cwms-data.usace.army/cwms-data/timeseries/Black Butte.Stor.Inst.~1Day.0.Calc-val?units=ft" + # will redirect to + curl "https://cwms-data.usace.army/cwms-data/v1/timeseries/Black Butte.Stor.Inst.~1Day.0.Calc-val?units=ft" + # after transition period, *IF* there is a new version + # will redirect to + curl "https://cwms-data.usace.army/cwms-data/v/timeseries/Black Butte.Stor.Inst.~1Day.0.Calc-val?units=ft" + # if possible, query parameters can be updated on behalf of the user + + +Opinions +======== + +Opinion 1 +--------- + +Summary: Current scheme is not working + +Author MikeNeilson, on behalf of others + +We have failed to properly handle existing usages while attempting to improve the overall design of the api +and have been breaking various downstream usages due to the confusion. Allowing the endpoints to be versioned allows +an easier time keeping existing behavior while also allowing more drastic improvements in usages to happen. + +Decision Status +=============== + +Status: accepted + +[comment:] <> (Status: request for comments | proposed | accepted | rejected | deprecated | superseded) + +References +========== + +1. https://www.youtube.com/watch?v=jmoxGJ_sLgU +2. https://newsletter.systemdesign.one/p/api-versioning +3. https://www.speakeasy.com/api-design/versioning diff --git a/docs/source/decisions/0005-data-authorization-middleware.md b/docs/source/decisions/0005-data-authorization-middleware.md new file mode 100644 index 0000000000..793f084749 --- /dev/null +++ b/docs/source/decisions/0005-data-authorization-middleware.md @@ -0,0 +1,748 @@ +# Authorization Middleware Implementation + +| Status | Implemented | +| :------------- | :------------------------------------------------------------------------------------------------------------------------- | +| **ADR #** | 0005 | +| **Author(s)** | Solid Logix Team
• [J. Hassan](https://github.com/jolitinh)
• [R. Cunningham](https://github.com/cunningryan)
• [V. Laxman](https://github.com/vairav)
• [M. Valenzuela](https://github.com/milver)
• [T. Boss](https://github.com/toddeboss)
• [C. Whitehead](https://github.com/ChristinaWhitehead) | +| **Sponsor** | HEC/USACE | +| **Date** | 7/6/2025 | +| **Supersedes** | Initial ADR draft | + +## Objective + +Implement a high-performance TypeScript-based authorization middleware service for the CWMS Data API that provides fine-grained, office-based access control to support HEC's transition from Oracle VPD to API-level authorization. The solution must support multiple user personas, maintain compatibility with existing authentication systems, and enable gradual migration from the current 32-database architecture to a single cloud-based system. + +**Scope**: While the CWMS Data API comprises 279 REST endpoints across 96 controller classes, this implementation focuses exclusively on timeseries resource endpoints. This targeted approach will establish the foundational authorization patterns and infrastructure that can be systematically extended to cover all remaining resources (locations, ratings, forecasts, water supply, etc.) in future phases. + +## Motivation + +### Current Limitations + +The existing Oracle VPD-based authorization system cannot support HEC's evolving requirements: + +- Limited to two Q-38 roles (modifier and admin) +- Cannot enforce the 7 distinct user personas defined in PWS Exhibit 3 +- No support for time-based embargo rules (7-day default) at API level +- Cannot implement data source restrictions (MANUAL vs AUTOMATED) +- No append-only enforcement for automated collection systems +- Cannot enforce 24-hour modification windows for Dam Operators +- No shift-hour validation (6am-6pm) for operational constraints +- No parameter-level filtering for external cooperators + +### Business Drivers + +- **Consolidation**: Moving from 32 independent databases to single cloud-based system +- **API-First**: Transition from direct database access to REST API only +- **Security**: Enhanced access control for critical water management infrastructure +- **Compliance**: NIST RMF and OWASP security requirements +- **Scalability**: Support for future growth and additional user types + +## User Benefit + +### Transparent to End Users + +- No changes to existing API endpoints or client applications +- Same authentication flows (API keys, JWT tokens) +- Backward compatibility during migration period + +### Enhanced for Administrators + +- Web-based permission management interface +- Granular office-based access controls +- Real-time policy updates without code changes +- Comprehensive audit trails for compliance +- Time-based embargo rule configuration (7-day default) +- User persona management for 7 distinct roles +- Data source tracking and enforcement + +### Operational Benefits + +- Independent scaling of authorization service +- Faster policy changes without database modifications +- Better performance through intelligent caching +- Detailed authorization decision logging + +## Design Proposal + +### Architecture Overview + +```mermaid +sequenceDiagram + participant Client as Client Application + participant AuthProxy as Authorization Service
(Transparent Proxy) + participant OPA as Policy Engine
(OPA) + participant Cache as Key-Value Cache + participant DataAPI as CWMS Data API
(Internal Only) + participant DB as Oracle Database + + Client->>AuthProxy: GET /cwms-data/timeseries?office=SPK + Note over Client,AuthProxy: Request hits Authorization Service
FQDN, i.e. https://cwms-api.xxx.gov/ + + AuthProxy->>AuthProxy: 1. Extract & Validate JWT + AuthProxy->>Cache: Check Policy Cache + + alt Cache Miss + AuthProxy->>OPA: Evaluate Authorization Policy + Note over OPA: Input: {
user: {offices: ["SPK"], roles: ["water_manager"]},
resource: {type: "timeseries", office: "SPK"},
action: {operation: "read"}
} + OPA-->>AuthProxy: {allow: true, context: {...}} + AuthProxy->>Cache: Store Decision (TTL: 5min) + else Cache Hit + Cache-->>AuthProxy: Cached Authorization Context + end + + AuthProxy->>AuthProxy: 2. Build Authorization Context + Note over AuthProxy: Single Header: x-cwms-auth-context
{user: {...}, constraints: {...}, filters: {...}} + + AuthProxy->>DataAPI: Forward Request + Original JWT + Auth Context + Note over AuthProxy,DataAPI: http://cwms-data-api:7000/cwms-data/timeseries
Headers: x-cwms-auth-context (JSON) + + DataAPI->>DataAPI: 3. Apply Authorization Filters + Note over DataAPI: Helper Library applies:
- Office filtering
- Basic embargo rules
- Sensitivity levels + + DataAPI->>DB: Execute Filtered Query + VPD + Note over DB: Combined filtering:
VPD + Authorization constraints + DB-->>DataAPI: Authorized Results Only + + DataAPI-->>AuthProxy: Response Data + + opt Complex Filtering Required + AuthProxy->>AuthProxy: 4. Apply Additional Filters + Note over AuthProxy: Complex embargo rules
Cross-resource dependencies
Persona constraints + end + + AuthProxy-->>Client: Final Authorized Response + Note over AuthProxy,Client: Transparent to client
Same API, enhanced security +``` + +### Transparent Proxy Data Flow + +```mermaid +graph TB + subgraph "External Layer" + C1[Client Applications] + C2[cwms-data-api-client] + end + + subgraph "Authorization Layer" + AP[Authorization Proxy Service] + OPA[Policy Engine] + Cache[Authorization Cache] + end + + subgraph "Application Layer (Internal)" + DA[CWMS Data API] + Helper[Authorization Helper Library] + end + + subgraph "Data Layer" + DB[(Oracle Database
with VPD)] + end + + C1 --> AP + C2 --> AP + + AP --> OPA + AP --> Cache + AP --> DA + + DA --> Helper + Helper --> DB +``` + +### Core Components + +#### 1. Authorization Middleware Service + +**Technology Stack:** + +- Node.js 22.x LTS or greater runtime +- TypeScript 5.x for type safety +- Fastify framework for high-performance HTTP handling +- Prometheus metrics for monitoring +- Pino for high-performance structured logging + +**Primary Modules:** + +- **Transparent Proxy Handler**: Generic HTTP proxy for timeseries-related CWMS API endpoints +- **Policy Evaluator**: OPA integration for authorization decisions with intelligent caching +- **Authorization Context Builder**: Creates structured authorization context for Java API integration +- **Selective Response Filter**: Applies complex filtering only when required (embargo, cross-resource, persona constraints) +- **Audit Logger**: Complete authorization decision tracking with request correlation + +#### 2. Policy Engine Integration (Open Policy Agent) + +**Policy Structure:** + +- Office-based access control (primary filtering mechanism) +- Role-based permissions for CRUD operations +- Time-based embargo rules with override capabilities (7-day default) +- Resource sensitivity filtering (public, internal, restricted, classified data) +- Persona-specific constraints per PWS Exhibit 3 + +**7 User Personas (PWS Exhibit 3):** + +1. **Anonymous/Public User**: Read-only access to public data after embargo period +2. **Dam Operator**: Manual data entry only, 24-hour modification window, shift hours (6am-6pm) +3. **Water Manager**: Embargo override capability, multi-office access, emergency operations +4. **Data Manager**: Bulk operations, cross-office access, delete permissions with justification +5. **Automated Collection System**: Append-only constraints, API key authentication, no historical edits +6. **Automated Processing System**: Derived data generation, cross-office read access, no raw data modification +7. **External Cooperator**: Parameter-specific access based on partnership agreements + +**Policy Data Management:** + +- Office configuration with embargo hours and regional groupings +- Permission matrices mapping roles to operations by resource type +- User persona definitions with specific constraints and capabilities +- Dynamic policy updates without code deployment + +**Specific Policy Requirements:** + +- **Time-based Embargo**: 7-day default embargo period, immediate access for Dam Operators +- **Data Source Validation**: Enforce MANUAL vs AUTOMATED data restrictions per persona +- **Append-Only Constraints**: Prevent historical data modification for automated systems +- **24-Hour Modification Window**: Allow Dam Operators to correct manual entries within 24 hours +- **Shift Hour Validation**: Restrict Dam Operator actions to 6am-6pm operational hours +- **Parameter-Level Filtering**: Whitelist specific parameters for external cooperators +- **Cross-Office Rules**: Enable multi-office access for Water Managers during emergencies +- **Audit Requirements**: Capture justification for Data Manager delete operations + +#### 3. Integration with Existing Systems + +**Current Authorization Architecture:** +- **CdaAccessManager**: Central authorization point for all 279 API requests +- **AuthDao**: Handles permission validation and VPD context setup across 96 controllers +- **Implementation Focus**: Timeseries endpoints (`/timeseries/*`) as foundation +- **VPD Integration**: Via `cwms_env.set_session_user_direct()` calls +- **Future Coverage**: Same architecture will extend to all 279 endpoints + +**CWMS Data API Integration:** + +- **Industry Standard Pattern**: Original JWT preserved in Authorization header +- **Enhanced Context**: Policy decisions in separate `x-cwms-auth-context` header +- **Zero Breaking Changes**: Existing JWT validation code remains unchanged +- **Authorization Helper Library**: Integration focused on timeseries controller endpoints + +**Authentication System Integration:** + +- **JWT Pass-Through**: Original Keycloak JWT forwarded unchanged +- **Existing Validation**: Current JWT validation code continues working +- **Enhanced Authorization**: Policy decisions added via separate header +- **API Key Support**: Existing authentication methods preserved +- **OAuth2 Compatibility**: Standard header patterns for future migration + +**Database Compatibility:** + +- Oracle VPD session context integration +- PostgreSQL RLS preparation with same integration pattern +- Database-agnostic context management +- Smooth migration path between database systems + +### Implementation Approach + +#### Phase 1: Transparent Proxy Foundation + +**Priority**: Authorization layer for timeseries endpoints + +- Fastify transparent proxy infrastructure with Pino logging +- Generic proxy handler supporting timeseries CWMS API endpoints +- Keycloak and OPA integration with intelligent caching +- Authorization Helper Library for timeseries controller integration +- Local development setup with Podman/Docker + +**Key Use Cases:** + +- **Manual Data Entry**: Dam Operator enters gate positions with 24-hour correction window +- **Automated Collection**: Sensor system appends readings without modifying historical data +- **Cross-Office Processing**: Automated system reads data from multiple offices for regional analysis +- **Partner Access**: External cooperator accesses specific parameters based on agreement + +#### Phase 2: Resource Extension (Future) + +Building on the timeseries foundation: + +- **Locations**: Apply same pattern to location endpoints +- **Ratings**: Extend authorization to rating curves and tables +- **Forecasts**: Add policies for forecast data access +- **Water Supply**: Implement accounting-specific rules +- **Administrative**: Secure configuration and metadata endpoints + +Each resource type will follow the established pattern, reusing the core infrastructure while adding resource-specific policies. + +**Key Use Cases:** + +- **Emergency Override**: Water Manager bypasses 7-day embargo during flood event + +#### Phase 3: Administration and Optimization + +- React-based admin UI for policy management +- VPD migration validation tools +- Advanced monitoring and analytics +- Preparation for potential data-api rebuild integration + +### Alternatives Considered + +#### Option 1: Traditional RBAC/ABAC (Database Tables) + +- **Pros**: Familiar patterns, database-backed permissions +- **Cons**: Cannot express complex time-based rules, difficult persona constraints +- **Rejected**: Insufficient for 7-day embargo, shift hours, append-only constraints +- **Technical Gap**: No native support for contextual decisions (emergency overrides) + +#### Option 2: Database-Level Enhancement (VPD Extension) + +- **Pros**: Familiar Oracle patterns, minimal API changes +- **Cons**: Cannot support API-level policies, migration complexity +- **Rejected**: Doesn't address API-first architecture requirements + +#### Option 3: API Gateway Plugins + +- **Pros**: Vendor support, no custom development +- **Cons**: Limited policy expressiveness, vendor lock-in +- **Rejected**: Cannot handle complex office-based ABAC requirements + +#### Option 4: Policy-Based Authorization with OPA (Selected) + +- **Pros**: Clean separation, policy-as-code, sub-5ms decisions, Git version control +- **Cons**: Additional service complexity, new technology stack +- **Selected**: Best alignment with PWS requirements and complex persona rules +- **Why OPA**: + - Expresses complex time-based rules (embargo, shift hours) naturally + - Supports contextual decisions (emergency overrides) with input data + - Policy testing with OPA Conftest ensures correctness + - Used by Netflix, Goldman Sachs, Pinterest for similar scale + - Performance: 1M+ decisions/second with proper caching + +### Performance Implications + +**Performance Targets:** + +- Authorization decision: <5ms (cached), <20ms (uncached) +- Response filtering: <10ms for complex rules +- Cache hit ratio: >95% +- Throughput: 15,000+ requests/second (Fastify capability: 48,000+ req/s) + +**Optimization Strategy:** + +- **Intelligent Caching**: Redis/ValKey for policy decisions, in-memory for static policies +- **Selective Filtering**: Apply response filtering only when required (strategy-based) +- **Connection Pooling**: HTTP/2 connection reuse to CWMS Data API +- **Streaming Responses**: For large datasets to minimize memory usage + +**Monitoring & Alerting:** + +- Real-time performance metrics +- Cache effectiveness monitoring +- Policy evaluation time tracking +- Error rate alerting + +### Dependencies + +**New Components:** + +- Open Policy Agent (OPA) - Policy engine +- Key-Value Cache (Redis, ValKey, etc.) - Distributed caching +- Transparent Proxy Service - Node.js 22.x LTS runtime environment + +**Existing Integration:** + +- Keycloak (Identity Provider) - No changes +- Traefik (API Gateway) - Configuration updates only +- Oracle Database - Parallel operation with VPD +- CWMS Data API - Minimal header-based integration + +**Development Dependencies:** + +- Jest for testing framework +- Supertest for API testing +- OPA Conftest for policy testing +- JMeter for load testing + +### Engineering Impact + +**Deployment:** + +- Separate Docker container (~100MB) +- Kubernetes-ready deployment manifests +- Independent CI/CD pipeline +- Blue-green deployment capability + +**Maintenance:** + +- Dedicated GitHub repository +- Semantic versioning +- API documentation with OpenAPI 3.0 + +**Testing Strategy:** + +- Unit tests: Policy logic validation +- Integration tests: End-to-end authorization flows +- Performance tests: Load and stress testing +- Security tests: OWASP compliance validation + +### Platforms and Environments + +**Local Development Environment Only:** + +- Podman/Docker containers +- ARM64/AMD64 multi-architecture support +- Resource requirements: 1-2 CPU cores, 1-2GB RAM +- All testing and validation in local environment + +**Development Tools:** + +- macOS/Linux development support +- Docker/Podman Compose for local orchestration +- Hot-reloading for rapid iteration +- Local testing with sample timeseries data + +### Best Practices + +**Security Best Practices:** + +- Default deny authorization stance +- Principle of least privilege +- Complete audit trail (immutable logs) +- Regular policy audits and reviews +- NIST RMF compliance + +**Operational Best Practices:** + +- Policy-as-code in Git repositories +- Automated policy testing in CI/CD +- Feature flag controlled rollouts +- Comprehensive monitoring and alerting + +### Compatibility + +**Backward Compatibility:** + +- No changes to existing API contracts +- All current authentication methods preserved +- VPD remains active during migration +- Gradual feature rollout with rollback capability + +**Forward Compatibility:** + +- PostgreSQL migration ready +- Cloud-native architecture +- Standard protocols (OAuth2, OIDC, REST) +- Extensible policy framework + +**Migration Compatibility:** + +- Parallel operation with existing VPD +- Office-by-office migration capability +- Emergency fallback to VPD-only mode +- Data consistency validation tools + +### User Impact + +**End Users (Zero Impact):** + +- Same API endpoints and authentication +- No client application changes required +- Transparent authorization enhancement + +**Data Administrators:** + +- New web-based access management UI +- Enhanced granular access controls +- Real-time policy updates +- Improved audit and reporting capabilities + +**Operations Team:** + +- New service to monitor and maintain +- Enhanced security posture +- Better troubleshooting capabilities +- Standardized policy management + +### Local Development & Testing Plan + +1. **Local Environment Setup** + + - Deploy using Docker/Podman Compose + - Configure all services (Database, API, Auth Service, OPA) + - Load sample timeseries test data + - Verify end-to-end connectivity + +2. **Timeseries Authorization Testing** + + - Implement authorization for timeseries endpoints + - Test all 7 user personas with timeseries data + - Validate embargo rules and time-based constraints + - Performance benchmarking in local environment + +3. **Integration Validation** + + - Test Authorization Helper Library with timeseries controllers + - Validate JWT pass-through and context headers + - Ensure VPD compatibility during transition + - Document integration patterns for future resources + +## Detailed Design + +### Implementation Strategy + +**Transparent Proxy Pattern:** + +- Authorization layer for `/cwms-data/timeseries/*` endpoints +- Policy-based access control with intelligent caching +- Single structured header for authorization context +- Selective response filtering for embargo and persona rules + +**Java API Integration:** + +- Authorization Helper Library for timeseries controllers +- Minimal changes to TimeSeriesController (2-3 lines) +- Structured authorization context via single HTTP header +- Focus on demonstrating pattern for future resources + +### Java API Integration Approach + +**Authorization Helper Library Pattern:** + +```java +@RestController +public class TimeSeriesController { + @Autowired + private CwmsAuthorizationHelper authHelper; + + @GetMapping("/timeseries") + public ResponseEntity> getTimeSeries( + String office, Context ctx) { + + QueryBuilder query = QueryBuilder.create().table("cwms_ts_data"); + query = authHelper.applyAuthorization(query, ctx); // Authorization applied + + List results = timeSeriesDao.execute(query); + results = authHelper.applyResponseFiltering(results, ctx); // Response filtering applied + + return ResponseEntity.ok(results); + } +} +``` + +**Integration Options:** + +1. **Dependency Injection**: `@Autowired` helper in controllers +2. **Annotation-Based**: `@CwmsAuthorized` with AOP interceptors +3. **Filter-Based**: Servlet filter for automatic processing + +_Detailed implementation patterns documented in separate design document._ + +### Future Architecture Considerations + +**Current Implementation**: Transparent proxy approach minimizing changes to existing Java API + +**Strategic Benefits**: + +- Compatible authorization patterns for future migration +- Policy consistency across implementations +- Team expertise development in modern authorization patterns + +## Implementation Scope + +### **Coverage (Current Scope)** + +- **API Endpoints**: Timeseries endpoints only (`/timeseries/*`) +- **Integration**: TimeSeriesController class demonstration +- **Client Compatibility**: Zero impact on existing cwms-data-api-client library +- **Environment**: Local development with Podman/Docker only + +### **Local Development Strategy** + +- **Helper Library**: Local JAR for timeseries integration +- **Docker Compose**: Complete local environment setup +- **Test Data**: Sample timeseries for all 7 personas +- **Documentation**: Integration guide for future resources + +## Success Criteria + +### **Functional Requirements** + +- Support all 7 user personas for timeseries data access +- Cover timeseries endpoints with consistent authorization +- Zero impact on existing cwms-data-api-client consumers +- Demonstrate integration pattern with TimeSeriesController + +### **Performance Requirements (Local Testing)** + +- Authorization decision: <5ms (cached), <20ms (uncached) +- API response time: <50ms total for timeseries queries +- Local throughput: 1,000+ requests/second +- Cache hit ratio: >95% +- Stable operation in local Docker/Podman environment + +### **Security Requirements** + +- Zero unauthorized access incidents +- Complete NIST RMF compliance +- Comprehensive audit trail for all authorization decisions +- Policy-driven access control with real-time updates + +### **Integration Requirements** + +- Transparent operation for timeseries API consumers +- Minimal changes to TimeSeriesController (<10 lines) +- Backward compatibility with existing authentication +- Clear pattern for extending to other resources + +## Conclusion + +This ADR addresses HEC's need for enterprise-scale authorization, demonstrated through a focused implementation for timeseries endpoints. The transparent proxy approach with Authorization Helper Library provides: + +### **Immediate Benefits** + +- **Zero Consumer Impact**: Existing timeseries clients work unchanged +- **Minimal Development Effort**: Simple integration pattern demonstrated +- **Local Testing**: Complete validation in Docker/Podman environment +- **Comprehensive Security**: Policy-driven access for 7 user personas + +### **Strategic Advantages** + +- **Proof of Concept**: Validates approach with timeseries data +- **Scalable Pattern**: Can extend to other resources as needed +- **Maintainable**: Clear separation of authorization concerns +- **Local First**: All development and testing in controlled environment + +### **Foundation for Full Coverage** + +This timeseries-focused implementation establishes: + +- **Reusable Infrastructure**: The authorization service, OPA integration, and caching layer work for all resources +- **Proven Patterns**: The Authorization Helper Library pattern demonstrated with TimeSeriesController can be replicated across all 96 controllers +- **Policy Framework**: OPA policies for timeseries establish templates for locations, ratings, forecasts, and other resources +- **Integration Blueprint**: The transparent proxy and header-based context passing will seamlessly support the remaining 278 endpoints + +By proving the authorization model with timeseries - the most complex and high-volume resource type - we create a solid foundation for systematically extending authorization to all CWMS Data API resources. + +### **Technical Excellence** + +- **Modern Stack**: Node.js 22.x LTS, Fastify, OPA for authorization +- **Industry Patterns**: Transparent proxy pattern proven at scale +- **Clean Integration**: Single header pattern for timeseries endpoints +- **Local Development**: Docker/Podman based development workflow + +This focused implementation on timeseries endpoints provides a solid foundation for validating the authorization approach while maintaining scope constraints for local development and testing. Once proven with timeseries, this exact same infrastructure and pattern can be systematically applied to authorize all 279 endpoints across the entire CWMS Data API, delivering comprehensive authorization coverage with minimal incremental effort. + +## Implementation Status + +This section documents the current state of the implementation as of January 2026. + +### Repository and Technology Stack + +The authorization middleware has been implemented in the `cwms-access-management` repository as an Nx monorepo with pnpm workspaces. The core technology choices align with the ADR proposal: + +- **Runtime**: Node.js 24.x with TypeScript 5.x +- **Framework**: Fastify with `@fastify/http-proxy` for transparent proxying +- **Logging**: Pino for high-performance structured logging +- **Cache**: Redis for distributed caching of user context and policy decisions + +### Transparent Proxy Implementation + +The authorization proxy service (`apps/services/authorizer-proxy/`) implements the transparent proxy pattern as specified: + +- Intercepts requests to `/cwms-data/*` endpoints +- Preserves original JWT in Authorization header +- Forwards requests to the internal CWMS Data API +- Adds authorization context via `x-cwms-auth-context` header + +### OPA Integration + +Open Policy Agent integration has been implemented with Rego policies supporting the 8 user personas defined in PWS Exhibit 3: + +- Anonymous/Public User +- Dam Operator +- Water Manager +- Data Manager +- Automated Collection System +- Automated Processing System +- External Cooperator +- System Administrator (added for administrative operations) + +The `/authorize` endpoint provides policy decisions for authorization requests, evaluating user context against OPA policies. + +### Whitelist Pattern for Selective Enforcement + +A whitelist-based approach controls which endpoints undergo OPA policy evaluation: + +- Configurable via `OPA_WHITELIST_ENDPOINTS` environment variable (JSON array) +- Default whitelist: `["/cwms-data/timeseries", "/cwms-data/offices"]` +- Non-whitelisted endpoints bypass OPA and are proxied directly +- Whitelist updates require container restart to take effect + +### Redis Caching Implementation + +User context caching has been implemented with Redis: + +- **Key Format**: `user:context:${username.toLowerCase()}` +- **TTL**: 30 minutes (1800 seconds) +- **Performance**: Approximately 10x improvement for repeated requests +- Cache stores user profile, roles, offices, and associated constraints + +### Authorization Context Header Format + +The `x-cwms-auth-context` header is implemented as a JSON structure: + +```json +{ + "policy": { + "allow": true, + "decision_id": "proxy-xxx" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "roles": ["cwms_user"], + "offices": ["SWT"], + "primary_office": "SWT" + }, + "constraints": { + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] + } +} +``` + +### Local Development Environment + +The implementation includes a complete local development setup using Podman: + +- **Authorization Proxy**: Port 3001 +- **OPA**: Port 8181 +- **Redis**: Port 6379 +- **Management UI**: Port 4200 (React-based administration interface) +- **CWMS Data API**: Port 7001 +- **Keycloak**: Port 8080 + +### Management Applications + +Two management applications have been implemented: + +- **Management UI** (`apps/web/management-ui/`): React 18, Vite 6, TanStack Query v5, Tailwind CSS 4 +- **Management CLI** (`apps/cli/management-cli/`): Node.js with Commander and Ink for terminal interface + +### Known Limitations + +- OPA policy updates require container rebuild (policy copied at build time) +- Full JWT validation not yet implemented in pass-through mode +- Java API helper library (`AuthorizationFilterHelper.java`) integration is prepared but not yet fully connected + +### Next Steps + +1. Create API key in CWMS Data API for authorization proxy authentication +2. Implement JWT token parsing with full validation in proxy +3. Connect Management CLI/UI to live API endpoints +4. Implement filtering constraints in Java API controllers +5. Configure OPA policy as volume mount for dynamic updates diff --git a/docs/source/decisions/0006-cda-authorization-filtering.md b/docs/source/decisions/0006-cda-authorization-filtering.md new file mode 100644 index 0000000000..d4e5fd417b --- /dev/null +++ b/docs/source/decisions/0006-cda-authorization-filtering.md @@ -0,0 +1,435 @@ +# CDA Authorization Filtering Integration + +| Status | Implemented | +| :------------- | :-------------------------------------------------------------------------------------------------------------------------- | +| **ADR #** | 0006 | +| **Author(s)** | Solid Logix Team
- [J. Hassan](https://github.com/jolitinh)
- [R. Cunningham](https://github.com/cunningryan)
- [V. Laxman](https://github.com/vairav)
- [M. Valenzuela](https://github.com/milver)
- [T. Boss](https://github.com/toddeboss)
- [C. Whitehead](https://github.com/ChristinaWhitehead) | +| **Sponsor** | HEC/USACE | +| **Date** | 2/2/2026 | +| **Supersedes** | N/A | + +## Objective + +Document the Java-side integration for authorization filtering in CWMS Data API. This ADR describes the implementation of helper classes that parse authorization context from the upstream proxy and generate database query conditions to enforce fine-grained access control at the data layer. + +## Motivation + +ADR 0005 established the Authorization Middleware architecture with a transparent proxy pattern. That ADR defined the overall architecture but left the Java API integration as an implementation detail. This ADR documents the completed implementation of the Java-side components that: + +- Parse the `x-cwms-auth-context` header passed by the authorization proxy +- Extract user identity and filtering constraints +- Generate JOOQ conditions for WHERE clauses that enforce access control +- Integrate with existing TimeSeriesController and TimeSeriesDaoImpl + +The implementation follows the principle that authorization decisions are made by the proxy (via OPA), while filtering constraints are applied at the database query level by the Java API. + +## User Benefit + +### For API Consumers + +- Transparent enforcement of access policies without client changes +- Consistent filtering behavior across all timeseries queries +- Clear error responses when access is denied + +### For API Developers + +- Simple integration pattern requiring minimal controller changes +- Reusable helper classes for future controller integration +- Feature flag to enable/disable authorization filtering +- Type-safe JOOQ conditions that integrate with existing query patterns + +### For Operations + +- Configurable via environment variable (`cwms.dataapi.access.management.enabled`) +- Detailed logging of authorization context and filter application +- Graceful degradation when header is absent + +## Design Proposal + +### x-cwms-auth-context Header Format + +The authorization proxy sends a JSON header containing user identity and filtering constraints. The header is only processed when access management is enabled via the `cwms.dataapi.access.management.enabled` configuration. + +```json +{ + "policy": { + "allow": true, + "decision_id": "proxy-abc123" + }, + "user": { + "id": "m5hectest", + "username": "m5hectest", + "email": "user@example.gov", + "roles": ["cwms_user", "ts_id_creator"], + "offices": ["SWT"], + "primary_office": "SWT", + "persona": "water_manager", + "region": "SWD" + }, + "constraints": { + "allowed_offices": ["SWT", "SPK"], + "embargo_rules": { + "SPK": 168, + "SWT": 72, + "default": 168 + }, + "ts_group_embargo": { + "Flood Control": 0, + "Public Safety": 0, + "default": 168 + }, + "embargo_exempt": false, + "time_window": { + "restrict_hours": 8 + }, + "data_classification": ["public", "internal"] + } +} +``` + +#### User Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique user identifier | +| `username` | string | Login username | +| `email` | string | User email address | +| `roles` | string[] | CWMS security group memberships | +| `offices` | string[] | Offices the user is associated with | +| `primary_office` | string | User's primary office assignment | +| `persona` | string | User persona type per PWS Exhibit 3 | +| `region` | string | Regional grouping (SWD, NWD, etc.) | + +#### Constraints Object Fields + +| Field | Type | Description | +|-------|------|-------------| +| `allowed_offices` | string[] | Offices user can access; `["*"]` means all | +| `embargo_rules` | object | Hours of embargo per office; `default` fallback | +| `ts_group_embargo` | object | Hours of embargo per TS group; `default` fallback | +| `embargo_exempt` | boolean | If true, bypass all embargo rules | +| `time_window` | object | Contains `restrict_hours` for lookback limit | +| `data_classification` | string[] | Allowed classification levels | + +### AuthorizationContextHelper Class + +Located at: `cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationContextHelper.java` + +This class parses the `x-cwms-auth-context` header and provides accessor methods for user information and constraints. + +#### Feature Flag + +The class respects the `cwms.dataapi.access.management.enabled` configuration: + +```java +private static final String ACCESS_MGMT_ENABLED_KEY = "cwms.dataapi.access.management.enabled"; +private static final boolean ACCESS_MGMT_ENABLED; + +static { + String envValue = System.getenv(ACCESS_MGMT_ENABLED_KEY); + String propValue = System.getProperty(ACCESS_MGMT_ENABLED_KEY); + String effectiveValue = propValue != null ? propValue : (envValue != null ? envValue : "false"); + ACCESS_MGMT_ENABLED = Boolean.parseBoolean(effectiveValue); +} +``` + +When disabled, the helper returns empty maps and null values, ensuring backward compatibility. + +#### Key Methods + +| Method | Return Type | Description | +|--------|-------------|-------------| +| `isEnabled()` | boolean | Static method returning feature flag state | +| `getUserId()` | String | User identifier | +| `getUsername()` | String | Login username | +| `getRoles()` | List | User's security group memberships | +| `getOffices()` | List | User's associated offices | +| `getPrimaryOffice()` | String | Primary office assignment | +| `getPersona()` | String | User persona type | +| `isEmbargoExempt()` | boolean | Whether user bypasses embargo | +| `hasRole(String)` | boolean | Check for specific role | +| `hasOfficeAccess(String)` | boolean | Check access to specific office | +| `buildOfficeFilter()` | String | Comma-separated office list for filtering | +| `isAuthorizationHeaderPresent()` | boolean | Whether header was parsed successfully | + +### AuthorizationFilterHelper Class + +Located at: `cwms-data-api/src/main/java/cwms/cda/helpers/AuthorizationFilterHelper.java` + +This class generates JOOQ `Condition` objects for WHERE clauses based on the constraints in the authorization context. + +#### Construction + +```java +// From Javalin context (typical usage) +AuthorizationFilterHelper authFilter = new AuthorizationFilterHelper(ctx); + +// From pre-parsed constraints (for testing) +AuthorizationFilterHelper authFilter = new AuthorizationFilterHelper(constraintsJsonNode); +``` + +#### Filter Methods + +##### Office Filtering + +```java +public Condition getOfficeFilter(Field officeField, String requestedOffice) +``` + +Generates conditions based on `allowed_offices` constraint: + +- If `allowed_offices` contains `"*"`: returns `noCondition()` (access to all) +- If `allowed_offices` is empty: returns `falseCondition()` (deny all) +- If `requestedOffice` provided and not in allowed: returns `falseCondition()` +- Otherwise: returns `officeField.in(allowedOffices)` + +##### Embargo Filtering + +```java +public Condition getEmbargoFilter(Field timestampField, + Field officeField, + String requestedOffice) +``` + +Applies time-based embargo rules: + +- If `embargo_exempt` is true: returns `noCondition()` +- If office-specific rule exists: applies that hours value +- Falls back to `default` hours if present +- Returns condition: `timestampField.lessThan(cutoffTimestamp)` + +The cutoff is calculated as: `now() - embargo_hours` + +Data NEWER than the cutoff is embargoed (restricted). + +##### TS Group Embargo Filtering + +```java +public Condition getTsGroupEmbargoFilter(Field timestampField, String tsGroupId) +public int getTsGroupEmbargoHours(String tsGroupId) +``` + +Applies embargo based on timeseries group membership: + +- Allows different embargo periods per TS group (e.g., Flood Control: 0 hours) +- Default embargo of 168 hours (7 days) when not specified + +##### Time Window Filtering + +```java +public Condition getTimeWindowFilter(Field timestampField, + Timestamp userRequestedBeginTime) +``` + +Restricts how far back users can query: + +- Uses `time_window.restrict_hours` to calculate cutoff +- Dam Operators limited to last 8 hours of data +- Returns condition: `timestampField.greaterOrEqual(cutoffTimestamp)` + +##### Data Classification Filtering + +```java +public Condition getClassificationFilter(Field classificationField) +``` + +Filters by data sensitivity level: + +- Uses `data_classification` array from constraints +- Returns: `classificationField.in(allowedClassifications).or(classificationField.isNull())` +- Null classifications are treated as accessible + +##### Combined Filter + +```java +public Condition getAllFilters(Field officeField, + Field timestampField, + Field classificationField, + String requestedOffice, + Timestamp userRequestedBeginTime) +``` + +Combines all filter conditions with AND logic for queries needing multiple constraints. + +### Integration with TimeSeriesController + +The TimeSeriesController creates both helper instances and uses them to apply authorization: + +```java +@Override +public void getAll(@NotNull Context ctx) { + try (final Timer.Context ignored = markAndTime(GET_ALL)) { + DSLContext dsl = getDslContext(ctx); + + AuthorizationContextHelper authHelper = new AuthorizationContextHelper(ctx); + AuthorizationFilterHelper authFilter = new AuthorizationFilterHelper(ctx); + + if (authHelper.isAuthorizationHeaderPresent()) { + logger.atInfo().log("Authorization context - User: %s, Offices: %s, Roles: %s", + authHelper.getUsername(), authHelper.getOffices(), authHelper.getRoles()); + } + + // Pass authFilter to DAO for query modification + TimeSeries timeSeries = dao.getTimeseries(page, pageSize, params, authFilter); + // ... + } +} +``` + +### Integration with TimeSeriesDaoImpl + +The DAO accepts the filter helper and applies conditions to the query: + +```java +@Override +public TimeSeries getTimeseries(String page, int pageSize, + TimeSeriesRequestParameters requestParameters, + AuthorizationFilterHelper authFilter) { + return getRequestedTimeSeries(page, pageSize, requestParameters, null, authFilter); +} + +protected TimeSeries getRequestedTimeSeries(String page, int pageSize, + @NotNull TimeSeriesRequestParameters requestParameters, + @Nullable FilteredTimeSeriesParameters fp, + @Nullable AuthorizationFilterHelper authFilter) { + // Build base query... + + if (authFilter != null && authFilter.hasAuthorizationContext()) { + Condition officeFilter = authFilter.getOfficeFilter(officeField, office); + Condition embargoFilter = authFilter.getEmbargoFilter(timestampField, officeField, office); + query = query.where(officeFilter.and(embargoFilter)); + } + + // Execute query... +} +``` + +## Alternatives Considered + +### Option 1: Servlet Filter Pattern + +Apply authorization filtering at the servlet filter level before requests reach controllers. + +- **Pros**: Single integration point, automatic for all endpoints +- **Cons**: No access to query context, cannot optimize filter conditions per endpoint +- **Rejected**: Filters operate at HTTP level without database context + +### Option 2: AOP Interceptors + +Use aspect-oriented programming to intercept DAO methods. + +- **Pros**: No controller changes required +- **Cons**: Complex configuration, harder to debug, magic behavior +- **Rejected**: Explicit is better than implicit for security-critical code + +### Option 3: Helper Classes (Selected) + +Provide helper classes that controllers explicitly instantiate and pass to DAOs. + +- **Pros**: Explicit integration, easy to understand, flexible per-endpoint customization +- **Cons**: Requires changes to each controller and DAO method +- **Selected**: Clear integration pattern that developers can understand and test + +## Performance Implications + +### Minimal Overhead + +- Header parsing: single JSON parse per request (~0.1ms) +- Condition generation: JOOQ DSL operations (~0.01ms) +- Feature flag check: static boolean check (~0ns) + +### Query Efficiency + +- Conditions integrate with JOOQ query builder +- Database optimizer can use indexes on office and timestamp columns +- No additional round trips to database + +### Caching Considerations + +- Authorization context is per-request (not cached in Java layer) +- Proxy-level caching of OPA decisions reduces upstream latency +- Database query caching unaffected + +## Dependencies + +### Existing Dependencies (No Changes) + +- Jackson ObjectMapper for JSON parsing +- JOOQ DSL for condition generation +- Java Logging for diagnostic output +- Javalin Context for HTTP header access + +### New Dependencies + +None required. Implementation uses existing dependencies. + +## Engineering Impact + +### Controller Changes + +Each controller that supports authorization filtering requires: + +1. Instantiate `AuthorizationContextHelper` and `AuthorizationFilterHelper` from context +2. Log authorization context when present (optional) +3. Pass filter helper to DAO methods + +Estimated: 5-10 lines per controller method. + +### DAO Changes + +Each DAO method that applies filtering requires: + +1. Accept optional `AuthorizationFilterHelper` parameter +2. Generate conditions using filter methods +3. Apply conditions to query WHERE clause + +Estimated: 10-20 lines per DAO method. + +### Testing Strategy + +1. **Unit Tests**: Test helper classes with mock JSON contexts +2. **Integration Tests**: Verify filter conditions generate valid SQL +3. **End-to-End Tests**: Validate filtering with authorization proxy + +## Compatibility + +### Backward Compatibility + +- Feature disabled by default (`cwms.dataapi.access.management.enabled=false`) +- When disabled, helpers return empty/null values +- Existing API behavior unchanged +- No header required for normal operation + +### Forward Compatibility + +- Header format can be extended with new fields +- Helpers ignore unknown fields +- New constraint types can be added without breaking existing code + +## Implementation Status + +### Completed + +- `AuthorizationContextHelper` class implementation +- `AuthorizationFilterHelper` class implementation +- TimeSeriesController integration +- TimeSeriesDaoImpl integration +- Feature flag configuration +- Logging for authorization context + +### Future Work + +- Extend to additional controllers (locations, ratings, forecasts) +- Add metrics for filter application +- Document integration pattern for other teams + +## Conclusion + +This ADR documents the implemented Java-side integration for authorization filtering in CWMS Data API. The helper class pattern provides a clear, testable approach to applying authorization constraints at the database query level. The implementation: + +- Respects the separation of concerns established in ADR 0005 +- Provides explicit integration that developers can understand +- Maintains backward compatibility when disabled +- Offers flexible per-endpoint customization + +The TimeSeriesController integration serves as the reference implementation for extending this pattern to additional CWMS Data API controllers. diff --git a/docs/source/decisions/0007-access-management-clients.md b/docs/source/decisions/0007-access-management-clients.md new file mode 100644 index 0000000000..b83ffa74c4 --- /dev/null +++ b/docs/source/decisions/0007-access-management-clients.md @@ -0,0 +1,460 @@ +# Access Management Clients + +| Status | Proposed | +| :------------- | :------------------------------------------------------------------------------------------------------------------------- | +| **ADR #** | 0007 | +| **Author(s)** | Solid Logix Team
[J. Hassan](https://github.com/jolitinh)
[R. Cunningham](https://github.com/cunningryan)
[V. Laxman](https://github.com/vairav)
[M. Valenzuela](https://github.com/milver)
[T. Boss](https://github.com/toddeboss)
[C. Whitehead](https://github.com/ChristinaWhitehead) | +| **Sponsor** | HEC/USACE | +| **Date** | 2/2/2026 | +| **Supersedes** | N/A | + +## Objective + +Document the management applications for CWMS access control, providing administrators with multiple interfaces to manage users, roles, and authorization policies. The management clients comprise a REST API, a web-based user interface, and a command-line interface, all designed to work together as a unified access management solution. + +## Motivation + +### Administrative Requirements + +The CWMS Data API authorization system requires administrative tooling for: + +- Managing user access and permissions across multiple offices +- Configuring role-based access control for the 7 user personas defined in PWS Exhibit 3 +- Viewing and auditing authorization policies +- Supporting both technical and non-technical administrators + +### Multiple Interface Requirements + +Different administrator personas require different interfaces: + +- **IT Administrators**: Prefer CLI for scripting and automation +- **Security Officers**: Require web UI for policy review and audit +- **Operations Team**: Need quick access to user management via web or CLI + +## User Benefit + +### Data Administrators + +- Centralized user and role management +- Visual policy viewer for audit and compliance +- Multiple interface options based on preference and workflow + +### IT Operations + +- CLI for scripting and automation workflows +- JSON output format for integration with other tools +- Batch operations support + +### Security and Compliance + +- Complete audit trail visibility +- Policy review capabilities +- User access reporting + +## Design Proposal + +### Architecture Overview + +```mermaid +graph TB + subgraph "Management Clients" + UI[Management UI
React + Vite] + CLI[Management CLI
Commander + Ink] + end + + subgraph "Management Layer" + API[Management API
Fastify] + end + + subgraph "Backend Services" + CDA[CWMS Data API] + OPA[Policy Engine] + end + + subgraph "Data Layer" + DB[(Oracle Database)] + end + + UI --> API + CLI --> API + API --> CDA + API --> OPA + CDA --> DB +``` + +### Data Flow + +```mermaid +sequenceDiagram + participant Admin as Administrator + participant Client as Management UI/CLI + participant MgmtAPI as Management API + participant CDA as CWMS Data API + participant DB as Oracle Database + + Admin->>Client: Request user list + Client->>MgmtAPI: GET /api/users + MgmtAPI->>CDA: GET /cwms-data/auth/users + CDA->>DB: Query at_sec_cwms_users + DB-->>CDA: User records + CDA-->>MgmtAPI: User data + MgmtAPI-->>Client: Formatted response + Client-->>Admin: Display users +``` + +### Core Components + +#### 1. Management API (management-api) + +**Technology Stack:** + +- Node.js 24.x runtime +- TypeScript 5.x for type safety +- Fastify framework for high-performance HTTP handling +- Pino for structured logging +- Zod for request/response validation + +**Primary Responsibilities:** + +- Provide RESTful endpoints for user and role management +- Aggregate data from CWMS Data API endpoints +- Transform and format responses for client consumption +- Handle authentication and session management + +**API Endpoints:** + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | /api/users | List all users with pagination | +| GET | /api/users/:id | Get user details by ID | +| POST | /api/users | Create new user | +| PUT | /api/users/:id | Update user | +| GET | /api/roles | List all roles | +| GET | /api/roles/:id | Get role details | +| POST | /api/roles | Create new role | +| PUT | /api/roles/:id | Update role | +| GET | /api/policies | List authorization policies | +| GET | /api/policies/:id | Get policy details | + +**Backend Integration:** + +The Management API connects to CWMS Data API endpoints rather than accessing Keycloak or the database directly. This design ensures: + +- Consistent data access through established API patterns +- Reuse of existing authentication and authorization logic +- Simplified deployment without direct database credentials + +#### 2. Management UI (management-ui) + +**Technology Stack:** + +- React 18.x with functional components and hooks +- Vite 6.x for build tooling and development server +- TypeScript 5.x for type safety +- TanStack Query v5 for server state management +- Tailwind CSS 4.x for styling +- React Router for navigation + +**Features:** + +| Feature | Description | +|---------|-------------| +| User List | Paginated table with search and filter capabilities | +| User Detail | View and edit user information, office assignments, roles | +| Role List | Display all available roles with descriptions | +| Role Detail | View role permissions and assigned users | +| Policy Viewer | Read-only view of OPA policies for audit purposes | +| Dashboard | Summary statistics and recent activity | + +**Component Architecture:** + +``` +src/ + components/ + users/ + user-list.tsx + user-detail.tsx + user-form.tsx + roles/ + role-list.tsx + role-detail.tsx + policies/ + policy-viewer.tsx + common/ + data-table.tsx + search-input.tsx + pagination.tsx + hooks/ + use-users.ts + use-roles.ts + use-policies.ts + services/ + api-client.ts + pages/ + users.tsx + roles.tsx + policies.tsx + dashboard.tsx +``` + +**State Management:** + +TanStack Query handles all server state with: + +- Automatic caching and background refetching +- Optimistic updates for improved UX +- Error handling and retry logic +- Request deduplication + +#### 3. Management CLI (management-cli) + +**Technology Stack:** + +- Node.js 24.x runtime +- TypeScript 5.x for type safety +- Commander.js for command parsing +- Ink for terminal UI components +- Zod for input validation + +**Commands:** + +``` +management-cli users list [--office ] [--format ] +management-cli users get [--format ] +management-cli roles list [--format ] +management-cli roles get [--format ] +management-cli policies list [--format ] +``` + +**Output Formats:** + +- **Table**: Human-readable formatted output for terminal use +- **JSON**: Machine-readable output for scripting and automation + +**Example Output (Table):** + +``` +Users +----- +Username Office Roles +----------- -------- --------------------------- +m5hectest SWT CWMS Users, TS ID Creator +l2hectest SPK CWMS Users, TS ID Creator +l1hectest SPL (none) +``` + +**Example Output (JSON):** + +```json +{ + "users": [ + { + "username": "m5hectest", + "office": "SWT", + "roles": ["CWMS Users", "TS ID Creator"] + } + ] +} +``` + +### Implementation Approach + +#### Phase 1: Management API Foundation + +- Implement core Fastify service structure +- Create user and role endpoints +- Integrate with CWMS Data API +- Add request validation and error handling +- Deploy as containerized service + +#### Phase 2: Management UI Development + +- Set up React + Vite project structure +- Implement user list and detail views +- Implement role list and detail views +- Add policy viewer component +- Integrate TanStack Query for data fetching +- Deploy as static assets or containerized service + +#### Phase 3: Management CLI Development + +- Create Commander-based CLI structure +- Implement user commands +- Implement role commands +- Add output format options +- Package as standalone executable + +### Alternatives Considered + +#### Option 1: Direct Database Access + +- **Pros**: Simpler architecture, fewer services +- **Cons**: Bypasses API authorization, requires database credentials in multiple places +- **Rejected**: Violates principle of API-first architecture + +#### Option 2: Keycloak Admin UI Only + +- **Pros**: No custom development required +- **Cons**: Limited to Keycloak capabilities, cannot show CWMS-specific data +- **Rejected**: Does not support CWMS-specific user attributes and office assignments + +#### Option 3: Management Clients via CWMS Data API (Selected) + +- **Pros**: Consistent access patterns, reuses existing authorization, single source of truth +- **Cons**: Requires Management API as intermediate layer +- **Selected**: Best alignment with API-first architecture and existing patterns + +### Performance Implications + +**Management API:** + +- Response time target: <100ms for list operations, <50ms for single record +- Caching strategy: In-memory cache for frequently accessed data (roles, offices) +- Connection pooling to CWMS Data API + +**Management UI:** + +- Initial load: <2 seconds +- Subsequent navigation: <500ms with client-side routing +- TanStack Query stale time: 5 minutes for user/role data + +**Management CLI:** + +- Command execution: <2 seconds for list operations +- Startup time: <500ms with bundled executable + +### Dependencies + +**New Components:** + +- Management API service (Fastify) +- Management UI application (React + Vite) +- Management CLI tool (Commander + Ink) + +**Existing Integration:** + +- CWMS Data API - Backend data source +- Authorization Proxy - Authentication passthrough +- Redis Cache - Session storage (optional) + +**Development Dependencies:** + +- Vitest for unit testing +- Playwright for E2E testing (UI) +- Supertest for API testing + +### Engineering Impact + +**Deployment:** + +- Management API: Docker container (~150MB) +- Management UI: Static assets (~5MB) or Nginx container +- Management CLI: Single executable (~200KB) + +**Maintenance:** + +- Shared codebase in Nx monorepo +- Common TypeScript types across all clients +- Unified testing and CI/CD pipeline + +### Platforms and Environments + +**Local Development:** + +- Podman/Docker containers +- Hot-reloading for UI and API development +- All services in docker-compose.podman.yml + +**Service Ports:** + +| Service | Port | +|---------|------| +| Management UI | 4200 | +| Management API | 3002 | +| Management CLI | N/A (local executable) | + +### Best Practices + +**Security:** + +- All requests authenticated via JWT +- Role-based access to management functions +- Audit logging for all write operations +- No direct database credentials in clients + +**User Experience:** + +- Consistent design language across UI and CLI +- Helpful error messages with suggested actions +- Confirmation dialogs for destructive operations +- Keyboard navigation support in UI + +**Code Quality:** + +- TypeScript strict mode enabled +- Shared types between API and clients +- Comprehensive test coverage +- Automated linting and formatting + +### Compatibility + +**Backward Compatibility:** + +- Management clients are new components, no backward compatibility concerns +- CWMS Data API endpoints remain unchanged + +**Forward Compatibility:** + +- API versioning support (/api/v1/users) +- Extensible command structure in CLI +- Component-based UI for easy feature additions + +## Implementation Status + +| Component | Status | Notes | +|-----------|--------|-------| +| Management API | In Progress | Refactoring to use CWMS Data API endpoints | +| Management UI | Development | React 18 + Vite 6 + TanStack Query v5 | +| Management CLI | Development | Commander + Ink, builds to 166KB executable | + +## Success Criteria + +### Functional Requirements + +- Support user CRUD operations through all three interfaces +- Support role viewing and assignment +- Provide policy viewer for audit purposes +- Consistent data across all interfaces + +### Performance Requirements + +- API response time: <100ms for list operations +- UI initial load: <2 seconds +- CLI command execution: <2 seconds + +### Usability Requirements + +- CLI commands follow standard Unix conventions +- UI follows accessibility guidelines (WCAG 2.1 AA) +- Clear error messages with actionable guidance + +## Conclusion + +The Access Management Clients provide a comprehensive suite of tools for administering CWMS access control. By offering three distinct interfaces (API, UI, CLI), the solution accommodates different administrator workflows and preferences while maintaining consistency through a unified backend. + +### Key Benefits + +- **Flexibility**: Multiple interface options for different use cases +- **Consistency**: All clients share the same backend and data model +- **Maintainability**: Shared codebase in Nx monorepo with common types +- **Security**: All access flows through authenticated API endpoints + +### Integration with Authorization System + +These management clients complement the authorization middleware described in ADR 0005 by providing the administrative interface for: + +- Managing users who will be subject to authorization policies +- Configuring roles that map to the 7 user personas +- Reviewing policies that govern access decisions + +Together with the authorization proxy and policy engine, the management clients complete the access control solution for the CWMS Data API. diff --git a/docs/source/decisions/adr-template.rst b/docs/source/decisions/adr-template.rst new file mode 100644 index 0000000000..1629e8787c --- /dev/null +++ b/docs/source/decisions/adr-template.rst @@ -0,0 +1,27 @@ +##### +Title +##### + + +Summary +======= + +Opinions +======== + +Opinion 1 +--------- + +Summary: + +Author + +descriptive text + +Decision Status +=============== + +[comment:] <> (Status: request for comments | proposed | accepted | rejected | deprecated | superseded) + +References +========== diff --git a/docs/source/decisions/index.rst b/docs/source/decisions/index.rst new file mode 100644 index 0000000000..cc4e5d92e4 --- /dev/null +++ b/docs/source/decisions/index.rst @@ -0,0 +1,26 @@ +################ +Design Decisions +################ + + +Overview +======== + + +Below are agreed upon decision choices regarding the usage of the API. +Please note that certain decisions may be agreed upon before implementation. +Whether or not a particular choice is implemented will be marked for each decision record. + +Some decisions may also be a proposal and marked appropriately. + +.. toctree:: + :maxdepth: 1 + :caption: Decisions + + Api Versioning <./0001-api-versioning.rst> + Data Versioning <./0002-data-versioning.rst> (rejected, remains for historical context.) + Catalogs and Search <./0003-searchability-and-catalogs.rst> + Versioning <./0004-versioning.rst> + Authorization Middleware <./0005-data-authorization-middleware.md> + CDA Authorization Filtering <./0006-cda-authorization-filtering.md> + Access Management Clients <./0007-access-management-clients.md> diff --git a/docs/source/design/composite-time-series.rst b/docs/source/design/composite-time-series.rst new file mode 100644 index 0000000000..6cd1b1018f --- /dev/null +++ b/docs/source/design/composite-time-series.rst @@ -0,0 +1,433 @@ +##################### +Composite Time Series +##################### + +Purpose +======= + +It is a challenge for users to identity what the correct authoritative time series is for a given measurement at a location. Additionally these time series often change over time, either being completely new or changing their interval as newer technologies become available. + +Gather an entire Period of Record for the value at a location is also rather difficult. And the Period of Record (POR) and "authoritative timeseries" may be one-in-the same. + + +Need +==== + +#. CWMS and Access-2-Water require a simple mechanism to allow users of data to retrieve the Authoritative Period of Record data for a given measurement without having to understand all of the possible component time series that may be involved. +#. Period-of-Record time series *should* not be created by duplicating data from the component time series and merging them into a new one. +#. The naming of the time series should fit within the existing CWMS Time Series Identifier design and not unreasonably interfere with existing usages. + + +Caveats +======= + +#. It is assumed that CWMS-Vue will, as-always, require updates to handle what is created here. + #. e.g. we're not going to let any current limitations of CWMS-Vue hinder our design. + + +Proposal +======== + +Description +----------- + +CWMS-Data-API (CDA) should handle a concept of a "Composite Time Series". Whether a Time Series is considered composite will be determined by some means (see naming options below). +Data Administrators will configure which Time Series, and the range there-in, are part of the composite time series. +CDA will use this composite time series definition to build an expand Time Series per query for the range of time requested. + +Additional names not used +------------------------- + +#. Virtual Time Series +#. Period of Record Time Series + +Both names have been discarded. We use "Virtual" in too many other places with a more direct meaning of that word. +For Period-of-Record, while that is the primary use-case, the concept is useful in other situations as well. + +Hence generically we have a "composite time series" + +Definitions +----------- + +Composite Time Series +~~~~~~~~~~~~~~~~~~~~~ + +A Time Series that is comprised of multiple same measure time series. For example, a river gage that has two sensors +only one of which is valid for certain conditions. + +Period of Record +~~~~~~~~~~~~~~~~ + +The Period of Record (POR, period-of-record) for a measurement (such as the Stage at a river or the pool elevation of +a dam) is ALL available (time,value) pairs since recording began until recording has ended or the most recent available +pair, regardless of changes in intervals or unavoidable changes in averaging. + +The POR is the "best" available combined dataset that would be desired for studies requiring all values for a given +location. + +However, "best" is subjective. The POR of data used to make a given decision may not be the same as data that has formal +validation. Additionally having a POR of the raw, unedited data, may be what a user studying data validation requires. + + +Naming Option 3, below, is selected for the path forward. A future design document will develop appropriate naming +to communicate the intent of any given composite time series, include period-of-record. + +Authoritative +~~~~~~~~~~~~~ + +An authoritative time series is a period-of-record time series with the additional constraint that is contains the best +official data. In other words the data that is determined to be "correct" by appropriate methods of validation. + +The data provide will have been validated or corrected after events when additional information become available. +The data may not match what was used at the moment a decision was made. + +.. NOTE:: + + At time of design we are considering a boolean flag to indicate whether a time series is "the authoritative correct" + time series or an arbitrary period-of-record. This may change in the future after the above mentioned group + determines an appropriate naming scheme. + +.. COMMENT:: + Responses to discussion that the above is derived from. + + + Yeah I agree that period of record is everything you have (or best available) for that site for as long as you have it. + To me, it makes sense if it's all Inst data (e.g. 8 am readings, 1 hour DCP, 15 minute DCP). + I think there could be an argument about mixing in averaged data....that's a harder sell for me. + For mixing and matching sensors, though that doesn't bother me. We are already taking huge leaps + of faith by using a single gage to represent the entire storage of reservoirs + + As far as providing daily average timeseries as an official period of record, you run into the issue + of a lot of years where you are averaging one instantaneous point which is not a great average. + So my dream was to just composite all the inst data. However, the load times in cwms for people wanting to quick view period of record probably won't allow that + + In LRL if we use "period of record" it's almost always referring to a flood control project and encompasses all recorded data from time of impoundment to present. + + If I am a user grabbing data from us, which I have done many time both for published research papers and in consulting. + I would want the POR to be the best available data for each time point from beginning of measurements to now. + We should provide the full record with mixed intervals. + And like the USGS we should provide the Daily Avg values, but that is a second step to what we are doing. + This is what your district is saying the water level was since you started recording data to today. + I don't care how you got the data unless I am going to do a detailed study of different sensors. + All of your sensors should be calibrated. If I need that information the user should be able to also see + the meta data for the composite timeseries and what individual timeseries it came from. + You are the the expert and should be able to provide the best available stage values and combine them into a single time series. + If someone came to you and said what was the level on XX Date/time what value would you give them? + That is the period of record. but for all dates and times. + + + +Axioms +------ + +#. Composite Time Series are Irregular +#. The definition of the composite time series is stored within the CWMS database +#. The members of a composite time series define a continuous range + #. The date ranges of a member *MUST* not overlap + #. Each member *MUST* have a start date + #. The last member *MAY* have an end date indicating no more data will be available for this location and measure. + #. Data may have gaps, an explanation range *SHOULD* be provided. For data with regular gaps, e.g. season gauges + a description should be provided in the notes. + Example: A Link to a Location Level can be provided if the seasonal timing is well known. This would let users + of the data now if the gap is missing data "an error" or if just out-of-service. +#. The members of a composite time series measure the same thing. (e.g. all members are Elevation, not some are elevation and some are stage.) +#. The parameter type (e.g. Instantaneous; Average; etc), interval, and duration of each member *MAY* be different. + + +Time Series Naming +------------------ + +Option 1 +~~~~~~~~ + +`...Composite.var.` + ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +| Element | Description | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Location Id |As the normal CWMS TS ID, the location for this measure | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter |As the normal CWMS TS ID, the measurement (e.g. Stage, Precip, Elevation, flow, etc) | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter Type |As Normal CWMS TS ID, Instantaneous, average, total, etc | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Interval -\> Composite| Marker that this time series does not have a fix information and is build of various member time series. | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Duration -\> var |Duration of average or total may change over time with new members, duration will be indicated in the member definition | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ +|Version |As Normal CWMS TS ID | ++----------------------+------------------------------------------------------------------------------------------------------------------------+ + + +Option 2 +~~~~~~~~ + +`..Composite.0.0.` + + ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +| Element | Description | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Location Id |As the normal CWMS TS ID, the location for this measure | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter |As the normal CWMS TS ID, the measurement (e.g. Stage, Precip, Elevation, flow, etc) | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter Type Composite|Marker that this time series does not have a fix information and is build of various member time series. | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Interval -\> 0 |Interval of data elements. may change over time with new members, duration will be indicated in the member definition | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Duration -\> 0 |Duration of average or total. may change over time with new members, duration will be indicated in the member definition| ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Version |As Normal CWMS TS ID | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ + +The zero's could also be var + + +Option 3 +~~~~~~~~ + +`.....Composite` + + ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +| Element | Description | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Location Id |As the normal CWMS TS ID, the location for this measure | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter |As the normal CWMS TS ID, the measurement (e.g. Stage, Precip, Elevation, flow, etc) | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter Type |Marker that this time series does not have a fix information and is build of various member time series. | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Interval |Interval of data elements. may change over time with new members, duration will be indicated in the member definition | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Duration |Duration of average or total. may change over time with new members, duration will be indicated in the member definition| ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Version |Composite or POR ... or check for composite at the front/back? | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ + +From Daniel + +Argument Against: the "Version" field it freeform and we often encode other information in it. +Argument Against above argument: That said, perhaps forcing the version to be "clean" is the right choice here. + + +Option 4 +~~~~~~~~ + +`.[Composite]....` + + ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +| Element | Description | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Location Id |As the normal CWMS TS ID, the location for this measure | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter |As the normal CWMS TS ID, the measurement (e.g. Stage, Precip, Elevation, flow, etc) | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter Type |Marker that this time series does not have a fix information and is build of various member time series. | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Interval |Interval of data elements. may change over time with new members, duration will be indicated in the member definition | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Duration |Duration of average or total. may change over time with new members, duration will be indicated in the member definition| ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Version |As Normal CWMS TS ID | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ + + +This form with something in [] has been discussed for embedded TimeZone and Offset information into the interval. Arguably this code go in any field. + + +Option 5 (Currently preferred) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.....` and/or arbitrary TS "alias" + + ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +| Element | Description | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Location Id |As the normal CWMS TS ID, the location for this measure | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter |As the normal CWMS TS ID, the measurement (e.g. Stage, Precip, Elevation, flow, etc) | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Parameter Type |Marker that this time series does not have a fix information and is build of various member time series. | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Interval |Interval of data elements. may change over time with new members, duration will be indicated in the member definition | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Duration |Duration of average or total. may change over time with new members, duration will be indicated in the member definition| ++------------------------+------------------------------------------------------------------------------------------------------------------------+ +|Version |As Normal CWMS TS ID | ++------------------------+------------------------------------------------------------------------------------------------------------------------+ + +However, on request for the timeseries the list of composite time series is consulted and used if present, otherwise passthrough to normal +time series retrieval. + + +Composite Time Series Definition +================================ + +.. code-block:: json + :caption: Schema + + { + "office": "", + "name": "", + "is-authoritative": true|false, + "seasonal-information": "/", // Optional, reference to location level that returns 0 for out-of-service + // or 1 for in-service + "members": [ + { + "time-series-id": "TS ID for this range", + "start": "start date of this", // Inclusive, ISO 8601 Full + "end": "end date of this range", // Exclusive, can be null + "notes": "text", + } + ] + // array above *should* be sorted by start when provided to user. + } + + +.. code-block:: json + :caption: Example (Using Option 5) + + // NOTE: While a specific location was used, the dates and time series are arbitrary and may not exist. + { + "office": "SPK", + "name": "Alder Springs.Precip-Cumulative.Inst.0.0.Best", + "is-authoritative": true, + "seasonal-information": null, // This location's measurement is not seasonal + "members": [ + { + "time-series-id": "Alder Springs.Precip-Cumulative.Inst.1DayLocal.0.Best", + "start": "1948-04-01T14:14:00Z", // Inclusive + "end": null, // Exclusive, can be null + "notes": "Drum Chart Recorder, Catch Tube", + }, + { + "time-series-id": "Alder Springs.Precip-Cumulative.Inst.1Hour.0.Best", + "start": "1962-04-01T14:14:00Z", // Inclusive + "end": null, // Exclusive, can be null + "notes": "Punched Tape Recorder, Catch Tube", + }, + { + "time-series-id": "Alder Springs.Precip-Cumulative.Inst.1DayLocal.0.Best", + "start": "1990-04-01T14:14:00Z", // Inclusive + "end": null, // Exclusive, can be null + "notes": "Digital Logger, Catch Tube", + }, + ] + // array above *WILL* be sorted by start when provided to user. + } + +Operations required: +-------------------- + +* Create +* Remove member (ts id + range) +* Add member +* List members +* Replace all members? +* Update member +* Delete + + +Events Requires: +---------------- + +* Composite Time Series created +* Composite Time Series deleted +* Composite Time Series modified + * Member added + * Member modified + * Member removed + +Immutable fields: + +Fields marked immutable above cannot be updated. At this time no field are thought to be immutable. + +Operations Prohibited: +---------------------- + +* Any direct manipulation of the underlying time series members. + +Example: one cannot `POST` values to a composite time series. Doing so will result in an HTTP 405 - Method Not Allowed +error. + +Composite Time Series Response +============================== + +.. code-block::jsonc + + { + // ... as current TimeSeries JSON + "composite-members-present": [ + // member definition from above + ] + } + + +Supported Operations: + +* Get, through existing TimeSeries classes. + + +Storage of member information +================================ + +#. Store in Clob as we refine the design - cache appropriately to avoid any major performance issues. +#. Create appropriate tables once the design is stable - still cache things. + +System responsibility for "knowing" to process composite. +========================================================= + +Time Series Catalog +------------------- + +Time Series Catalog should show composite time series and allow searching by "authoritative" + +TimeSeries DTO +-------------- + +Add nullable "members" property. + +TimeSeriesDao +------------- + +If the system sees the "Composite" marker/determines is composite retrieve the members for the range and build the time series. + +.. NOTE:: + Considering the user may request the *entire* Period-of-record, + start the retrieval in a job queue, and return a status URL to the user for future download. I have see such mechanism + for bulk data in other systems. Maybe return an "I'm working on it variant" that the controller can know how to format. + + Perhaps we do this for data beyond "x amount"? + +Error handling and other conditions. +==================================== + +Versioned (date) time series +---------------------------- + +It is an error to specify a Version (date) when requesting composite data. Only the latest data of each member will +be returned. + +Datum conversions +----------------- + +Retrievers of the Period-of-Record *SHOULD* be able to retrieve the data as a single datum. Composite retrieval should respond + as https://github.com/USACE/cwms-data-api/issues/1102 and convert each member as appropriate + + +On the saving of a composite definition +--------------------------------------- + +The even if only a single member is added, the full definition needs to be check to ensure the ranges are still non overlapping. + +References +========== + +#. https://github.com/USACE/cwms-data-api/discussions/956 +#. https://github.com/USACE/cwms-data-api/issues/955 +#. https://www.hec.usace.army.mil/confluence/spaces/CWMS/pages/290456000/Virtual+Timeseries +#. https://discourse.hecdev.net/t/period-of-record-timeseries/3859/2 \ No newline at end of file diff --git a/docs/source/design/index.rst b/docs/source/design/index.rst new file mode 100644 index 0000000000..8f91deba52 --- /dev/null +++ b/docs/source/design/index.rst @@ -0,0 +1,12 @@ +################ +Design Documents +################ + +The follow pages formally document current and proposed designs +relating to operations and usage of data. + +.. toctree:: + :maxdepth: 2 + :caption: Introduction + + Composite Time Series <./composite-time-series.rst> diff --git a/docs/source/endpoints/timeSeries_endpoints/index.rst b/docs/source/endpoints/timeSeries_endpoints/index.rst index c88ca240c1..ebec552753 100644 --- a/docs/source/endpoints/timeSeries_endpoints/index.rst +++ b/docs/source/endpoints/timeSeries_endpoints/index.rst @@ -1,19 +1,11 @@ .. _timeseries-endpoints: -Time Series Endpoints +TimeSeries Endpoints ======================= - -`CWMS database - Time Series Definition `_ - -`CDA Time Series Endpoints Wiki `_ - - - - .. note:: - The documentation is a work in progress. This section currently includes the below TimeSeries endpoints and focuses + This documentation is a work in progress. This section currently includes the below TimeSeries endpoints and focuses on the GET methods and their parameters. POST, PATCH, and DELETE methods and their specific parameters are coming soon. @@ -24,30 +16,14 @@ Browse Time Series GET Endpoints: .. toctree:: :maxdepth: 1 - TimeSeries Basics <./timeseries_basics.rst> + TimeSeries Basic Information <../../data/timeseries.rst> + Common Parameter Definitions <./shared_definitions.rst> + Common Reasons for Parameter Usage <./shared_when_to_use.rst> /timeseries /timeseries/recent /timeseries/profile - /timeseries/profile/{location-id}/{parameter-id} + /timeseries/profile/{location-id}/{parameter-id} /timeseries/profile-parser - /timeseries/profile-parser/{location-id}/{parameter-id} + /timeseries/profile-parser/{location-id}/{parameter-id} /timeseries/profile-instance - /timeseries/profile-instance/{location-id}/{parameter-id}/{version} - Shared Definitions <./shared_definitions.rst> - - -.. note:: - - Using the intersphinx extension, a reference instead of the hard-coded link is preferred for maintainability: - - See #:ref:#`cwmsdb:time-series` for details. - - Once the build on the CWMS Database docs is live and stable each section that needs to be referenced - will need a label added to it in the cwms-database docs like this: - - .. code-block:: - - .. _time-series: - - Time Series - --------------- \ No newline at end of file + /timeseries/profile-instance/{location-id}/{parameter-id}/{version} diff --git a/docs/source/endpoints/timeSeries_endpoints/shared_definitions.rst b/docs/source/endpoints/timeSeries_endpoints/shared_definitions.rst index 9e70ac65b0..010feb2970 100644 --- a/docs/source/endpoints/timeSeries_endpoints/shared_definitions.rst +++ b/docs/source/endpoints/timeSeries_endpoints/shared_definitions.rst @@ -6,7 +6,7 @@ Shared timeseries endpoint parameters Shared parameter definitions ---------------------------- -This section lists and describes the parameters that are shared across multiple TimeSeries endpoints. +This section lists and describes common parameters that are used by multiple TimeSeries endpoints. If the parameter is only used by a single endpoint, please refer to that endpoint's documentation for details. If a shared parameter has endpoint-specific behavior or constraints, those details will be noted in the individual endpoint documentation. @@ -14,59 +14,115 @@ endpoint documentation. .. _def-end: end - End date/time for the time series data to stop. + The date and time marking the end of the time window for data included in the response. + The format for this field is ISO 8601 extended with optional offset and timezone. + + .. code-block:: sql + + Example: + YYYY-MM-ddThh:mm:ss[Z[VV]] + 2021-06-10T13:00:00-07:00 OR 2025-10-25T12:25:00Z + + .. note:: + Detailed documentation for Timestamps usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/timestamps in a future release. + .. _def-location-id: location-id - Description pending. + `CWMS database - Location Naming `_ + `CWMS database - Location Definition `_ .. _def-location-mask: location-mask - Description pending. + A regular expression used to filter the location name associated with the queried time series data. See the Regex + documentation page for more information on usage: + + .. note:: + Detailed documentation for Regex usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/regexp in a future release. .. _def-office: office - The organizational context used to scope data access and defaults. Some endpoints infer a default office; you can also specify it explicitly. + The organizational context used to scope data access and defaults. Some endpoints infer a default office; + you can also specify it explicitly. .. _def-office-mask: office-mask - Description pending. + A regular expression used to filter the office identifier associated with the queried time series data. + See the Regex documentation page for more information on usage: + + .. note:: + Detailed documentation for Regex usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/regexp in a future release. .. _def-page: page - Page token for paginated endpoints. Use with next/previous links to continue a result set. + Page token for paginated endpoints. Value to use is provided by the `next-page` entry of a qualifying query response. .. _def-page-size: page-size - Maximum number of items per page (server may enforce an upper bound). + Maximum number of items per page (server may enforce an upper bound). Further results must be accessed using the \ + `next-page` value provided in the response of queries that return more results than will fit on one page. .. _def-parameter-id: parameter-id - Description pending. + A text identifier specifying the type of data measured by the time series, such as "Flow", "Stage", "Elev", etc. + + .. note:: + This link will take you to the Parameter Types definition. Scroll up one section to see the Parameter Definition. + `CWMS database - parameter types `_ + + As soon as this link is repaired, we will replace the above link with the correct one: + `CWMS database - parameters `_ .. _def-parameter-id-mask: parameter-id-mask - Description pending. + A regular expression used to filter the parameter of the queried time series data. + See the Regex documentation for more information on usage: + + .. note:: + Detailed documentation for Regex usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/regexp in a future release. + +.. _def-start: + +start/begin + The date and time marking the beginning of the time window for data included in the response. + The format for this field is ISO 8601 extended with optional offset and timezone. + + .. code-block:: sql + + Example: + YYYY-MM-ddThh:mm:ss[Z[VV]] + 2021-06-10T13:00:00-07:00 OR 2025-10-25T12:25:00Z + + .. note:: + Detailed documentation for Timestamps usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/timestamps in a future release. .. _def-timezone: timezone - Description pending. + The timezone to use for retrieved time data, such as "UTC", "America/Los_Angeles", etc. .. _def-unit: -unit - Deprecated; prefer units or unit-system. +unit `(Deprecated, prefer units or unit-system)` + The unit system or specific unit to convert the response data into. Available unit systems are SI or EN. + Examples of other units are m, ft, m3, etc. + For reference: `CWMS database - units `_ .. _def-version-date: version-date - Description pending. \ No newline at end of file + A date associated with a time series to make identification of the most recent data possible. + Often uses the forecast date. diff --git a/docs/source/endpoints/timeSeries_endpoints/shared_when_to_use.rst b/docs/source/endpoints/timeSeries_endpoints/shared_when_to_use.rst new file mode 100644 index 0000000000..727979e183 --- /dev/null +++ b/docs/source/endpoints/timeSeries_endpoints/shared_when_to_use.rst @@ -0,0 +1,63 @@ +Shared Time Series Examples of When to Use +============================================ + +.. _when_start: + +start/begin + To limit the results to be after a specified date and time. + +.. _when_end: + +end + To limit the results to be before a specified date and time. + +.. _when_office: + +office + To limit your results to a specific office if there \ + are multiple time series with the same identifier across multiple offices, for example with a daily forecast that \ + more than one office may generate. This can also help improve query response time for large datasets. + +.. _when_location_id: + +location-id + To specify the location for which you want to retrieve time series or profile data, \ + such as a specific river gauge or reservoir. + +.. _when_parameter_id: + +parameter-id + To identify the specific parameter combination \ + associated with the desired profile or profile parser, e.g. `Flow-Evap`. + +.. _when_page: + +page + To reach a specific page in the set of results to get results that were not able to fit in the previous page. + +.. _when_page_size: + +page_size + To specify the number of results you wish to receive \ + from a single query, such as for the purpose of \ + receiving a small set of results out of many, e.g. using `50` to get 50 out of 5000 total results.\ + Further results may be available on a subsequent page of the same length. + +.. _when_office_mask: + +office-mask + To limit results to a specific office, such as `SPK`, or to offices \ + starting with `S` using `S*`. + +.. _when_location_mask: + +location-mask + To limit results to a specific location or pattern, \ + for example limiting results to locations containing `River` using `*River*`. + +.. _when_parameter_id_mask: + +parameter-id-mask + To limit results to a specific parameter or pattern, \ + for example limiting results to parameters starting with `Flow` using `Flow*`. \ + For multiple parameters, a mask may look like `Depth-Temperature` or `*-Temperature`. \ No newline at end of file diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-byID.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-byID.rst new file mode 100644 index 0000000000..9cecf475f2 --- /dev/null +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-byID.rst @@ -0,0 +1,88 @@ +.. _timeseries-profile-byID-endpoint: + +TimeSeries — GET /timeseries/profile/{location-id}/{parameter-id} +=================================================================== + + +What it does +------------ +Retrieves a specific time series profile for a given location and parameter. + +A "profile" stores multiple values for each point in time. Think of it as a combination of time series data +and paired data. It’s called a “profile” because it’s often used for things like temperature profiles in lakes over time. + +In profile data, there is: + +- An independent variable (e.g., Depth) +- Dependent variables (e.g., Temperature at those depths over time) + +The independent variable describes the dependent variables and is NOT tied to time. For example, in a lake temperature +profile, depths are the independent variable, and temperatures at those depths are the dependent variables. + +The endpoint path must include the independent variable name, followed by a dash "-", and then the dependent variable +name. For example: `/Depth-Temperature/`. + +Important Rules: + +- The number of independent variables must stay consistent across the entire dataset. For example, if you have 10 depth + readings, you must keep 10 depth readings for the entire dataset (missing values can be marked as such). +- You must include an independent variable set, even if it's not used. For example, if you store multiple time series + values, you still need an independent variable array, (it can contain zeros if unused). + +Profile data can include quality indicators and notes, similar to other time series data. It can use regular or +irregular intervals, with minute granularity (default) or second granularity. + +When to use +----------- +- You wish to retrieve a specific time series profile and already know the location and parameter. + + +.. csv-table:: GET /timeseries/profile/{location-id}/{parameter-id} - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 40, 20, 65 + + location-id,":ref:`def-location-id`","Yes", ":ref:`when_location_id`" + parameter-id,":ref:`def-parameter-id`","Yes", ":ref:`when_parameter_id`" + office,":ref:`def-office`","", ":ref:`when_office`" + +Examples +-------- + +1. | The user wants to retrieve the temperature profile at various depths: + | (**parameter-id**) :code:`Depth-Temperature` + | + | for the RIVER-STATION1 location: + | (**location-id**) :code:`RIVER-STATION1` + | + | but is unsure of which office to use. Query includes required path parameters, `location-id` and `parameter-id`. + + .. code-block:: bash + + GET /timeseries/profile/[location-id]/[parameter-id] + + .. code-block:: bash + + GET /timeseries/profile/RIVER-STATION1/Depth-Temperature + +2. The user reviews the results from the previous example query and decides to to narrow the search to the `HQ` office. + Query remains the same: + + | (**parameter-id**) :code:`Depth-Temperature` + | + | (**location-id**) :code:`RIVER-STATION1` + | + | but adds the optional query parameter, `office`: + | (**office**) :code:`HQ` + + .. code-block:: urlencoded + + GET /timeseries/profile/[location-id]/[parameter-id]?office=[office] + + .. code-block:: urlencoded + + GET /timeseries/profile/LOC123/Depth-Temperature?office=HQ + + +See the consolidated API documentation: :doc:`/api-references`. + +.. include:: /_includes/feedback_button.rst \ No newline at end of file diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance-byID.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance-byID.rst new file mode 100644 index 0000000000..7db14a7297 --- /dev/null +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance-byID.rst @@ -0,0 +1,226 @@ +.. _timeSeries-profile-instance-byID-endpoint: + +TimeSeries — GET /timeseries/profile-instance/{location-id}/{parameter-id}/{version} +====================================================================================== + +What it does +------------ +Retrieves a specific versioned profile instance for a location and parameter. + +When to use +----------- +- To retrieve a specific instance of a time series profile with a known version, parameter, and location. + + +.. csv-table:: GET /timeseries/profile-instance{location-id}/{parameter-id}/{version} - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 50, 20, 60 + + location-id,":ref:`def-location-id`","Yes", ":ref:`when_location_id`" + parameter-id,":ref:`def-parameter-id`","Yes", ":ref:`when_parameter_id`" + version,"`CWMS database - version `_ + This is a text value that is independent of the version date.","Yes", "To specify the desired version of the \ + profile instance provided when storing the instance." + office,":ref:`def-office`","Yes", ":ref:`when_office`" + timezone,":ref:`def-timezone`","", "Use to convert the resulting data into a specific timezone, such as \ + `America/Los_Angeles`." + version-date,":ref:`def-version-date`","", "To specify a desired version date associated with the profile \ + instance. Not including this parameter will result in the response containing the instance with the most recent \ + version date" + unit,":ref:`def-unit` + Units must be compatible with desired instance data. For this endpoint, they are comma separated for the two \ + associated parameters, e.g. `m,F` for the `Depth-Temperature` parameter","Yes", "To specify the \ + desired units for the instance response." + start-time-inclusive,"Resulting data includes data from the exact start time of the time window (true/false).","\ + ", "To choose whether data points on the configured start-time parameter will be included in the response, \ + such as for the purpose of calculating averages for a time window." + end-time-inclusive,"Resulting data includes data from the exact end of the time window (true/false).","", "To \ + choose whether data points on the configured end-time parameter will be included in the response, such as for the \ + purpose of calculating averages for a time window." + previous,"Include the previous time window of the time series profile instance (true/false).","", "To include data \ + starting at the closest timestamp before the specified `start` date and time." + next,"Include the next time window of the time series profile instance (true/false).","", "To include data up to \ + the closest timestamp after the specified `end` parameter date and time." + max-version,"Use the most recent version date (true/false). Only for time series utilizing dates in the version.","\ + ", "To retrieve the instance with the latest version date (true), or to use in combination with a specific \ + version date (false) by providing a date using the version-date parameter." + start,":ref:`def-start`","Yes", ":ref:`when_start`" + end, ":ref:`def-end`", "Yes", ":ref:`when_end`" + page,":ref:`def-page`","", ":ref:`when_page`" + page-size,":ref:`def-page-size`","", ":ref:`when_page_size`" + + +Examples +-------- +1. The user wants to retrieve the latest profile instance data for a specific location and parameter within a defined + time range. They specify the location ID as LOC123: + + | (**location-id**) :code:`LOC123` + | + | the parameter ID as Depth-Temperature: + | (**parameter-id**) :code:`Depth-Temperature` + | + | and the version as CWMS: + | (**version**) :code:`CWMS` + | + | They also set the office to HQ: + | (**office**) :code:`HQ` + | + | and define the time window from October 1, 2025, at 06:00 UTC: + | (**start**) :code:`2025-10-01T06:00:00Z` + | + | to January 21, 2026, at 18:00 UTC: + | (**end**) :code:`2026-01-21T18:00:00Z` + | + | They choose units of meters (m) and Fahrenheit (F) for the response: + | (**unit**) :code:`m,F` + + .. code-block:: bash + + GET /timeseries/profile-instance/[location-id]/[parameter-id]/[version]?office=[office]&start=[start]&end=[end]&unit=[unit] + + .. code-block:: urlencoded + + GET /timeseries/profile-instance/LOC123/Depth-Temperature/CWMS?office=HQ&start=2025-10-01T06:00:00Z&end=2026-01-21T18:00:00Z&unit=m,F + +2. The user wants to retrieve a specific version of the profile instance by providing a version date. + They specify the same location ID, parameter ID, version, office, units, and time window as before: + + | (**location-id**) :code:`LOC123` + | + | (**parameter-id**) :code:`Depth-Temperature` + | + | (**version**) :code:`CWMS` + | + | (**office**) :code:`HQ` + | + | (**start**) :code:`2025-10-01T06:00:00Z` + | + | (**end**) :code:`2026-01-21T18:00:00Z` + | + | (**unit**) :code:`m,F` + | + | but this time they include the version-date parameter set to January 1, 2026, at 12:00 UTC: + | (**version-date**) :code:`2026-01-01T12:00:00Z` + | + | and the max-version parameter set to False: + | (**max-version**) :code:`False` + + .. code-block:: bash + + GET /timeseries/profile-instance/[location-id]/[parameter-id]/[version]?office=[office]&start=[start]&end=[end]&unit=[unit]&version-date=[version-date]&max-version=[True/False] + + .. code-block:: urlencoded + + GET /timeseries/profile-instance/LOC123/Depth-Temperature/CWMS?office=HQ&unit=m,F&start=2025-10-01T06:00:00Z&end=2026-01-21T18:00:00Z&version-date=2026-01-01T12:00:00Z&max-version=False + +3. The user wants to retrieve the most recent version of the profile instance. + They specify the same location ID, parameter ID, version, office, units, and time window as before: + + | (**location-id**) :code:`LOC123` + | + | (**parameter-id**) :code:`Depth-Temperature` + | + | (**version**) :code:`CWMS` + | + | (**office**) :code:`HQ` + | + | (**start**) :code:`2025-10-01T06:00:00Z` + | + | (**end**) :code:`2026-01-21T18:00:00Z` + | + | (**unit**) :code:`m,F` + | + | but this time they set the max-version parameter to True and do not include a version-date parameter: + | (**max-version**) :code:`True` + + .. code-block:: bash + + GET /timeseries/profile-instance/[location-id]/[parameter-id]/[version]?office=[office]&start=[start]&end=[end]&unit=[unit]&max-version=[True/False] + + + .. code-block:: urlencoded + + GET /timeseries/profile-instance/LOC123/Depth-Temperature/CWMS?office=HQ&unit=m,F&start=2025-10-01T06:00:00Z&end=2026-01-21T18:00:00Z&max-version=True + +4. The user wants to retrieve the most recent version of the profile instance with a specific time zone and + inclusivity settings for the time window. They want to include data points that occur at the beginning and end of + the provided time window. They specify the same location ID, parameter ID, version, office, units, + and time window as before: + + | (**location-id**) :code:`LOC123` + | + | (**parameter-id**) :code:`Depth-Temperature` + | + | (**version**) :code:`CWMS` + | + | (**office**) :code:`HQ` + | + | (**start**) :code:`2025-10-01T06:00:00Z` + | + | (**end**) :code:`2026-01-21T18:00:00Z` + | + | (**unit**) :code:`m,F` + | + | but this time they set the timezone parameter to Pacific (Los Angeles): + | (**timezone**) :code:`America/Los_Angeles` + | + | and set both the start-time-inclusive and end-time-inclusive parameters to True: + | (**start-time-inclusive**) :code:`True` + | + | (**end-time-inclusive**) :code:`True` + | + | They want to limit the result size to 15 entries, so they include the page-size parameter: + | (**page-size**) :code:`15` + + .. code-block:: bash + + GET /timeseries/profile-instance/[location-id]/[parameter-id]/[version]?office=[office]&start=[start]&end=[end]&unit=[unit]&page-size=[page-size]&timezone=[timezone]&start-time-inclusive=[True/False]&end-time-inclusive=[True/False] + + + .. code-block:: urlencoded + + GET /timeseries/profile-instance/LOC123/Depth-Temperature/CWMS?office=HQ&start=2025-10-01T06:00:00Z&end=2026-01-21T18:00:00Z&unit=m,F&page-size=15&timezone=America/Los_Angeles&start-time-inclusive=True&end-time-inclusive=True + +5. The user wants to retrieve the most recent version of the profile instance with a specific time zone and + inclusivity settings for the time window. They specify the same location ID, parameter ID, version, office, units, + and time window as before: + + | (**location-id**) :code:`LOC123` + | + | (**parameter-id**) :code:`Depth-Temperature` + | + | (**version**) :code:`CWMS` + | + | (**office**) :code:`HQ` + | + | (**start**) :code:`2025-10-01T06:00:00Z` + | + | (**end**) :code:`2026-01-21T18:00:00Z` + | + | (**unit**) :code:`m,F` + | + | but this time they set the timezone parameter to UTC: + | (**timezone**) :code:`UTC` + + and include the next parameter set to True to include the single time step of data after the end + date of the specified time window: + + | (**next**) :code:`True` + | + | They want to limit the result size to 15 entries, so they include the page-size parameter: + | (**page-size**) :code:`15` + + .. code-block:: bash + + GET /timeseries/profile-instance/[location-id]/[parameter-id]/[version]?office=[office]&start=[start]&end=[end]&unit=[unit]&page-size=[page-size]&timezone=[timezone]&next=[True/False] + + + .. code-block:: urlencoded + + GET /timeseries/profile-instance/LOC123/Depth-Temperature/CWMS?office=HQ&start=2025-10-01T06:00:00Z&end=2026-01-21T18:00:00Z&unit=m,F&page-size=15&timezone=UTC&next=True + + +See the consolidated API documentation: :doc:`/api-references`. + +.. include:: /_includes/feedback_button.rst \ No newline at end of file diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance.rst index 58a87a44b8..1440e43936 100644 --- a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance.rst +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-instance.rst @@ -1,9 +1,26 @@ -TimeSeries — GET /timeSeries/profile-instance +.. _timeseries-profile-instance-endpoint: + +TimeSeries — GET /timeseries/profile-instance ============================================= What it does ------------ -Enumerate profile instances (actual profile datasets) and their versions. Use this to discover which instances exist before fetching a specific one. +Lists all available profile instances (datasets) and their versions. Use it to see what instances exist before +retrieving a specific one. + +A profile instance is data recorded by one full cycle of the sensor as it sweeps through the key parameter range. This +includes the timestamps for each reading. If the recorded data includes parameters not specified in the time series +profile definition, the profile instance data stored in the CWMS database will be a subset of the overall recorded data. + +Profile instances are identified by location, key parameter, version identifier, first recorded time, and version date. +This means: + +- Multiple first recorded times can exist for a single location, key parameter, version identifier, and version date + combination. +- Multiple version identifiers can exist for a single location, key parameter, first recorded time, and version date + combination. +- Multiple version dates can exist for a single location, key parameter, version identifier, and first recorded time + combination When to use ----------- @@ -11,22 +28,52 @@ When to use - Find the latest or a specific version of an instance -.. csv-table:: /timeseries/profile-instanceEndpoint Parameters - :header: "Parameter", "Description", "Required" - :widths: 20, 60, 15 +.. csv-table:: GET /timeseries/profile-instance - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 50, 20, 60 + + version-mask,"A regular expression used to filter the version field for time series retrieval.","", "To \ + limit results to a specific version, such as `CWMS`." + office-mask,":ref:`def-office-mask`","", ":ref:`when_office_mask`" + location-mask,":ref:`def-location-mask`","", ":ref:`when_location_mask`" + parameter-id-mask,":ref:`def-parameter-id-mask`","", ":ref:`when_parameter_id_mask`" + +.. note:: + Detailed documentation for Regex usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/regexp in a future release. - version-mask,"","" - office-mask,":ref:`def-office-mask`","" - location-mask,":ref:`def-location-mask`","" - parameter-id-mask,":ref:`def-parameter-id-mask`","" Examples -------- -- List instances for a parameter at locations starting with ABC: +1. The user wants to see all available profile instances in the CWMS database. + + .. note:: + Depending on the contents of the database, this query may return a large number of results. + Consider filtering the results by known values such as the office, location, or parameter ID. + + .. code-block:: bash + + GET /timeseries/profile-instance + +2. | The user wants to see all available profile instances for offices starting with `MV`, such as `MVR` and `MVS`: + | (**office-mask**) :code:`MV*` + + .. code-block:: urlencoded + + GET /timeseries/profile-instance?office-mask=MV* + +3. | The user wants to list all profile instances at the HQ office: + | (**office-mask**) :code:`HQ` + | + | at locations starting with "ABC": + | (**location-mask**) :code:`ABC*` + | + | for parameter combinations starting with "Flow" (such as Flow-Freq [Flow-Frequency] and Flow-Evap [Flow-Evaporation]): + | (**parameter-id-mask**) :code:`Flow*` -.. code-block:: sql + .. code-block:: urlencoded - GET /timeseries/profile-instance?location-mask=ABC*¶meter-id-mask=Flow*&office=HQ + GET /timeseries/profile-instance?location-mask=ABC*¶meter-id-mask=Flow*&office-mask=HQ See the consolidated API documentation: :doc:`/api-references`. diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser-byID.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser-byID.rst new file mode 100644 index 0000000000..cde371355b --- /dev/null +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser-byID.rst @@ -0,0 +1,43 @@ +.. _timeSeries-profile-parser-byID-endpoint: + +TimeSeries — GET /timeseries/profile-parser/{location-id}/{parameter-id} +========================================================================= + +What it does +------------ +Retrieves parser information associated with a specific profile identified by location and parameter. + +Profile parsers can be defined to enable storage of profile instance data directly from the text output of data loggers. + +When to use +----------- +- You need the parser used for a specific profile to understand how values are interpreted. + + +.. csv-table:: GET /timeseries/profile-parser{location-id}/{parameter-id} - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 40, 20, 60 + + location-id,":ref:`def-location-id`","Yes", ":ref:`when_location_id`" + office,":ref:`def-office`","Yes", ":ref:`when_office`" + parameter-id,":ref:`def-parameter-id`","Yes", ":ref:`when_parameter_id`" + +Examples +-------- +1. | The user wants to retrieve the profile parser for Flow-Evaporation data: + | (**parameter-id**) :code:`Flow-Evap` + | + | at the STREAM12 location: + | (**location-id**) :code:`STREAM12` + | + | for the LRL office: + | (**office**) :code:`LRL` + + .. code-block:: urlencoded + + GET /timeseries/profile-parser/STREAM12/Flow-Evap?office=LRL + + +See the consolidated API documentation: :doc:`/api-references`. + +.. include:: /_includes/feedback_button.rst \ No newline at end of file diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser.rst index eaab6a423a..56c15d6b29 100644 --- a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser.rst +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile-parser.rst @@ -1,31 +1,57 @@ -TimeSeries — GET /timeSeries/profile-parser +TimeSeries — GET /timeseries/profile-parser ============================================= What it does ------------ -List or inspect available profile parsers (the logic that interprets profile data formats). +Lists or inspects available profile parsers, these are the rules or logic used to interpret profile data formats. + +Profile parsers explain how to read text output from data loggers. While parameter values for profile instance data +are stored in CWMS time series, the data often arrives in non-standard format. For example: + +- GOES transmissions decoded by third-party software +- Data stored directly in the database or encoded as SHEF for CWMS processing. + +Profiles consist of identifiable instances instead of a continuous time series. Profile parsers make it possible to +store these instances by using the text exported from a data logger. When to use ----------- -- Discover parser options before requesting a specific profile parser by IDs +- To discover available parsers before requesting a specific profile parser by its ID. -.. csv-table:: /timeseries/profile Endpoint Parameters - :header: "Parameter", "Description", "Required" - :widths: 20, 60, 15 +.. csv-table:: GET /timeseries/profile - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 40, 20, 60 - office-mask,":ref:`def-office-mask`","" - location-mask,":ref:`def-location-mask`","" - parameter-id-mask,":ref:`def-parameter-id-mask`","" + office-mask,":ref:`def-office-mask`","", ":ref:`when_office_mask`" + location-mask,":ref:`def-location-mask`","", ":ref:`when_location_mask`" + parameter-id-mask,":ref:`def-parameter-id-mask`","", ":ref:`when_parameter_id_mask`" Examples -------- -- List available parsers for your office: +1. The user wants to see all available profile parsers in the system. + + .. code-block:: + + GET /timeseries/profile-parser + +2. | The user wants to see all available profile parsers in the HQ office: + | (**office-mask**) :code:`HQ` + + .. code-block:: urlencoded + + GET /timeseries/profile-parser?office-mask=HQ + +3. | The user wants to see all available profile parsers for the Area-Evaporation parameter: + | (**parameter-id-mask**) :code:`Area-Evap` + | + | at location names ending with "BASIN": + | (**location-mask**) :code:`*BASIN` -.. code-block:: sql + .. code-block:: urlencoded - GET /timeseries/profile-parser?office=HQ + GET /timeseries/profile-parser?parameter-id-mask=Area-Evap&location-mask=*BASIN See the consolidated API documentation: :doc:`/api-references`. diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile.rst index 887154f845..67e90e5405 100644 --- a/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile.rst +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-profile.rst @@ -1,33 +1,88 @@ -TimeSeries — GET /timeSeries/profile +.. _timeseries-profile-endpoint: + +TimeSeries — GET /timeseries/profile ===================================== What it does ------------ -List or discover available time series profiles. Use this to see what profiles exist before requesting a specific profile by IDs. +Lists available time series profiles. Use this to see what profiles exist before requesting a specific +profile by ID. + +A time series profile is a collection of timestamped values for a set of parameters associated with a specific location +and key parameter. These profiles are primarily used for the storage of depth-linked water quality data in reservoirs, +but can also store other types of profiles such as height-linked meteorological data. + +The timestamped values are stored as standard CWMS time series. For each location and key parameter, the CWMS time +series includes values from all profile instances that share the same combination of location, key parameter, and +version identifier. +(:ref:`See timeseries/profile-instance endpoint for details. `) + +Profile definitions are linked to a specific combination of location and key parameter. Only one profile definition +can exist for each location-key parameter pair. When to use ----------- -- Inventory the profiles available for your office +- Catalog the profiles available for your office - Filter by location or parameter to narrow results -.. csv-table:: /timeseries/profile Endpoint Parameters - :header: "Parameter", "Description", "Required" - :widths: 20, 60, 15 +.. csv-table:: GET /timeseries/profile - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 40, 20, 65 + + location-mask,":ref:`def-location-mask`","", ":ref:`when_location_mask`" + office-mask,":ref:`def-office-mask`","", ":ref:`when_office_mask`" + page,":ref:`def-page`","", ":ref:`when_page`" + page-size,":ref:`def-page-size`","", ":ref:`when_page_size`" + parameter-id-mask,":ref:`def-parameter-id-mask`","", ":ref:`when_parameter_id_mask`" - location-mask,":ref:`def-location-mask`","" - office-mask,":ref:`def-office-mask`","" - page,":ref:`def-page`","" - page-size,":ref:`def-page-size`","" - parameter-id-mask,":ref:`def-parameter-id-mask`","" Examples -------- -- List profiles for locations starting with ABC: +1. | The user wants to retrieve the profiles of all parameters from the `HQ` office: + | (**office**) :code:`HQ` + + but is unsure of the location name. They know the location name starts with `ABC`, so they use a wildcard search: + + | (**location-mask**) :code:`ABC*` + + .. code-block:: urlencoded + + GET /timeseries/profile?location-mask=ABC*&office=HQ + +2. | The user wants to retrieve the profiles for the elevation parameter: + | (**parameter-id-mask**) :code:`Elev` + | + | across all offices starting with `S`, such as `SPK`, `SRL`, and `SWT`, so they use a wildcard search for the office: + | (**office-mask**) :code:`S*` + + .. code-block:: urlencoded + + GET /timeseries/profile?office-mask=S*¶meter-id-mask=Elev + +3. | The user wants to list the profiles of all parameters at the `SPK` office: + | (**office-mask**) :code:`SPK` + | + | but only wants to see 100 results at a time. + | (**page-size**) :code:`100` + + .. code-block:: urlencoded + + GET /timeseries/profile?office-mask=SPK&page-size=100 + +4. The user wants to list the following page of results from the previous query, using the `next-page` value + of `t!qqoLun283` returned in the prior response. The query remains the same: + + | (**office-mask**) :code:`SPK` + | + | (**page-size**) :code:`100` + | + | but adds the `page` parameter: + | (**page**) :code:`t!qqoLun283` -.. code-block:: sql + .. code-block:: urlencoded - GET /timeseries/profile?location-mask=ABC*&office=HQ + GET /timeseries/profile?office-mask=SPK&page-size=100&page=t!qqoLun283 See the consolidated API documentation: :doc:`/api-references`. diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries-recent.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries-recent.rst index 7231b7b2df..485a657fb0 100644 --- a/docs/source/endpoints/timeSeries_endpoints/timeSeries-recent.rst +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries-recent.rst @@ -1,9 +1,11 @@ -TimeSeries — GET /timeSeries/recent +TimeSeries — GET /timeseries/recent =================================== What it does ------------ -Return the most recent value(s) from one or more time series without downloading a historical range. +Returns the most recent value(s) from one or more time series without downloading a historical range. + +Retrieves time series data from 28 days before to 14 days after the current date. When to use ----------- @@ -11,25 +13,52 @@ When to use - Health checks and alerts for current conditions -.. csv-table:: /timeseries/recent Endpoint Parameters - :header: "Parameter", "Description", "Required" - :widths: 20, 60, 15 +.. csv-table:: GET /timeseries/recent - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 60, 25, 55 - category-id,"","" - group-id,"","" - ts-ids,"","" - unit-system,"SI or EN or other","" - office,":ref:`def-office`","" + category-id, "The text identifier for the time series category defined in the CWMS database for a specific time \ + series.","", "To limit results to a specific assigned time series category." + group-id, "The text identifier of the time series group defined in the CWMS database for a specific time series.","\ + Only if ts-ids are NOT provided", "To limit results to a specific assigned time series group." + ts-ids, "`CWMS database - time series `_","\ + Only if group-id is NOT provided", "To get the recent data for the specified time series." + unit-system, "SI or EN, default: EN","", "To convert response data to a particular unit system." + office, ":ref:`def-office`","", ":ref:`when_office`" Examples -------- -- Latest values for a list of series IDs: +1. | The user wants to retrieve the recent time series data for the specified time series IDs of + | `STATION1.Flow.Inst.15Minutes.0.CWMS` and `STATION2.Stage.Inst.15Minutes.0.CWMS`: + | (**ts-ids**) :code:`STATION1.Flow.Inst.15Minutes.0.CWMS,STATION2.Stage.Inst.15Minutes.0.CWMS` + | + | and they want the data to be in the Imperial unit system: + | (**unit-system**) :code:`EN` + + .. code-block:: urlencoded + + GET /timeseries/recent?ts-ids=STATION1.Flow.Inst.15Minutes.0.CWMS,STATION2.Stage.Inst.15Minutes.0.CWMS&unit-system=EN + +2. | The user wants to retrieve the recent time series data for all time series in the `CALC3` time series group: + | (**group-id**) :code:`CALC3` + + .. code-block:: urlencoded + + GET /timeseries/recent?group-id=CALC3 -.. code-block:: sql +3. | The user wants to retrieve the recent time series data for all time series in the `CALC3` time series group: + | (**group-id**) :code:`CALC3` + | + | and in the `COMPUTE` time series category: + | (**category-id**) :code:`COMPUTE` + | + | for the `HQ` office: + | (**office**) :code:`HQ` - GET /timeseries/recent?ts-ids=STATION1.Flow.Inst.15Minutes.0.CWMS,STATION2.Stage.Inst.15Minutes.0.CWMS&unit=ft + .. code-block:: urlencoded + GET /timeseries/recent?group-ide=CALC3&category-id=COMPUTE&office=HQ See the consolidated API documentation: :doc:`/api-references`. diff --git a/docs/source/endpoints/timeSeries_endpoints/timeSeries.rst b/docs/source/endpoints/timeSeries_endpoints/timeSeries.rst index a6bce8a978..2f4496c550 100644 --- a/docs/source/endpoints/timeSeries_endpoints/timeSeries.rst +++ b/docs/source/endpoints/timeSeries_endpoints/timeSeries.rst @@ -1,3 +1,5 @@ +.. _timeSeries_endpoint: + TimeSeries — GET /timeseries ============================== @@ -15,34 +17,114 @@ When to use - Compare units or intervals -.. csv-table:: GET Parameters - :header: "Parameter", "Description", "Required" - :widths: 20, 60, 15 - - begin, "also start. need to verify and add to shared def.", "" - datum, "", "" - end, ":ref:`def-end`", "" - format, "", "" - include-entry-date, "", "" - name(required), "", "Yes" - office, "see :ref:`def-office`", "" - page, ":ref:`def-page`", "" - page-size, ":ref:`def-page-size`", "" - timezone, ":ref:`def-timezone`", "" - trim, "", "" - unit, "deprecated, prefer units", "" - units, "SI or EN or other. Need to verify", "" - version-date, ":ref:`def-version-date`", "" - +.. csv-table:: GET /timeseries - Endpoint Parameters + :header: "Parameter", "Description", "Required", "When to Use" + :widths: 30, 60, 20, 60 + + begin, ":ref:`def-start`", "", ":ref:`when_start`" + datum, "The standardized reference system used for either vertical measurements. \ + Examples: NAVD88, NGVD29, LOCAL, etc.", "", "To retrieve measurements in a specified system." + end, ":ref:`def-end`", "", ":ref:`when_end`" + format, "The desired response format. Usage differs between endpoints. See note below.", "", "Use this \ + to force the format provided in the response." + include-entry-date, "Include timestamps for when each data point was added to the CWMS database (true/false).", "\ + ", "To determine when each time series data point was stored." + name, "The text representation of the unique time series identifier.", "Yes", "To \ + differentiate the specific time series data you desire to retrieve." + office, ":ref:`def-office`", "", ":ref:`when_office`" + page, ":ref:`def-page`", "", ":ref:`when_page`" + page-size, ":ref:`def-page-size`", "", ":ref:`when_page_size`" + timezone, ":ref:`def-timezone`", "", "To retrieve data points in a timezone that works best with \ + your use case, such as your local timezone." + trim, "Trim missing values from the beginning and end of the retrieved values (true/false).", "", "To leave out \ + missing values to get only the stored values of the time series data set." + unit, ":ref:`def-unit`", "", "Do not use this parameter, instead use the 'units' parameter below." + units, "`CWMS database - units `_", "", "To \ + convert the retrieved values into a desired unit, such as retrieving elevation data in feet (ft) instead \ + of meters (m)." + version-date, ":ref:`def-version-date`", "", "To limit results to a specific version, \ + such as when multiple versions of a time series exist with different associated forecast dates." + + +.. note:: + Detailed documentation for Legacy Format Responses for the `format` parameter in CDA is currently + in development and will be available at https://cwms-data.usace.army.mil/cwms-data/legacy-format + in a future release. Examples ---------- -- Latest 24 hours in metric units: - -.. code-block:: sql - - GET /timeseries?name=STATION1.Flow.Inst.15Minutes.0.CWMS&begin=now-24H&unit=m3/s +1. | The user wants to retrieve flow data for `STATION1` at 15-minute intervals from the time series: + | (**name**) :code:`STATION1.Flow.Inst.15Minutes.0.CWMS` + | + | from October 12, 2025 at 12:35 PM UTC onward: + | (**begin**) :code:`2025-10-12T12:35:00Z` + | + | with the values converted to cubic meters per second: + | (**units**) :code:`m3/s`. + + .. code-block:: urlencoded + + GET /timeseries?name=STATION1.Flow.Inst.15Minutes.0.CWMS&begin=2025-10-12T12:35:00.000Z&units=m3/s + +2. | The user wants to retrieve elevation data for `STATION2` at 15-minute intervals from the time series: + | (**name**) :code:`STATION2.Elev.Avg.15Minutes.1Day.CWMS` + | + | with the values converted to feet: + | (**units**) :code:`ft` + | + | and using the NAVD88 datum: + | (**datum**) :code:`NAVD88` + + .. code-block:: urlencoded + + GET /timeseries?name=STATION2.Elev.Avg.15Minutes.1Day.CWMS&datum=NAVD88&units=ft + +3. | The user wants to retrieve temperature data for `STATION3` at 12-hour intervals from the time series: + | (**name**) :code:`STATION3.Temp.Inst.12Hour.1Month.CWMS` + | + | using the version date of October 1, 2025 at 12:00 PM UTC: + | (**version-date**) :code:`2025-10-01T12:00:00Z` + | + | and limiting results to the office `NWDP`: + | (**office**) :code:`NWDP` + + .. code-block:: urlencoded + + GET /timeseries?name=STATION3.Temp.Inst.12Hour.1Month.CWMS&version-date=2025-10-01T12:00:00Z&office=NWDP + +4. | The user wants to retrieve area data for `STATION4` at 1-day intervals from the time series: + | (**name**) :code:`STATION4.Area.Total.1Day.1Week.Surface-CWMS` + | + | with 25 results per response: + | (**page-size**) :code:`25` + | + | in the Pacific timezone (Los Angeles): + | (**timezone**) :code:`America/Los_Angeles` + | + | and including the entry dates of each data point: + | (**include-entry-date**) :code:`True` + + .. code-block:: urlencoded + + GET /timeseries?name=STATION4.Area.Total.1Day.1Week.Surface-CWMS&page-size=25&timezone=America/Los_Angeles&include-entry-date=True + +5. The user wants to retrieve the following page of results for the above query with a page value of `rGfes*720SJK` + provided by the response from the previous query (`next-page`): + + | (**page**) :code:`rGfes*720SJK` + | + | (**name**) :code:`STATION4.Area.Total.1Day.1Week.Surface-CWMS` + | + | (**page-size**) :code:`25` + | + | (**timezone**) :code:`America/Los_Angeles` + | + | (**include-entry-date**) :code:`True` + + .. code-block:: urlencoded + + GET /timeseries?name=STATION4.Area.Total.1Day.1Week.Surface-CWMS&page-size=25&timezone=America/Los_Angeles&include-entry-date=True&page=rGfes*720SJK See the consolidated API documentation: :doc:`/api-references`. diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 2f5e3a3e8c..7737a98a9f 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -6,7 +6,7 @@ Frequently Asked Questions 1. **Question:** What is CWMS Data API? **Answer:** CWMS Data API is a web service that provides access to water management data. Review the :ref:`glossary` - for more information. + for more information. 2. **Question:** How do I get started with CWMS Data API? @@ -15,14 +15,14 @@ Frequently Asked Questions 3. **Question:** What kind of data can I access using CWMS Data API? **Answer:** You can access various types of water management data including reservoir levels, inflows, outflows, - and more. + and more. 4. **Question:** Is there any authentication required to use the API? **Answer:** Yes, you need to obtain an API key to authenticate your requests that require writing, updating, or - deleting data, or if the endpoint contains sensitive data (API Keys, etc). + deleting data, or if the endpoint contains sensitive data (API Keys, etc). 5. **Question:** Where can I find more information? **Answer:** This site contains user information but we also have a - `SwaggerUI `_ for developers to experiment with the endpoints. \ No newline at end of file + `SwaggerUI `_ for developers to experiment with the endpoints. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index c2bfdf963c..9b84789941 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -38,8 +38,23 @@ Welcome to CWMS Data API documentation! .. toctree:: - :maxdepth: 1 - :caption: Alternative Topics + :maxdepth: 3 + :caption: Access Management - Alternative Topics <./alternative-topics/index.rst> + Access Management <./access-management/index.md> + +.. toctree:: + :maxdepth: 1 + :caption: Alternative Topics + + Alternative Topics <./alternative-topics/index.rst> + +.. toctree:: + :maxdepth: 1 + :caption: Design Documents + + Design <./introduction/design.rst> + Decision Records <./decisions/index.rst> + Design Documents <./design/index.rst> + diff --git a/docs/source/libraries/java.rst b/docs/source/libraries/java.rst index f94f6e318e..ee1a88f88b 100644 --- a/docs/source/libraries/java.rst +++ b/docs/source/libraries/java.rst @@ -4,5 +4,3 @@ CWMS Java Client Library - cwmsjava ===================================== This page is coming soon. Please check back later for updates and new content. - - diff --git a/docs/source/parameters/timeSeries_params/allTSParameters.rst b/docs/source/parameters/timeSeries_params/allTSParameters.rst index d7df4959fc..adc9fdcab2 100644 --- a/docs/source/parameters/timeSeries_params/allTSParameters.rst +++ b/docs/source/parameters/timeSeries_params/allTSParameters.rst @@ -3,68 +3,86 @@ /timeseries endpoint parameters ================================ -This page serves as a reference for all parameters used across the TimeSeries endpoints. It may be useful only for -building the documentation and then be removed later. - - - begin - - category-id - - datum - - end - - end-time-inclusive - - format - - group-id - - include-entry-date - - location-id: - - https://cwms-database.readthedocs.io/en/latest/naming.html#locations - - https://cwms-database.readthedocs.io/en/latest/locations.html#overview - - location-mask - - max-version - - name - - next - - office: - - https://cwms-database.readthedocs.io/en/latest/naming.html#offices - - office-mask - - page - - page-size - - parameter-id - - parameter-id-mask - - previous - - start - - start-time-inclusive - - timezone - - trim - - ts-ids - - unit - - unit-system - - units: - - https://cwms-database.readthedocs.io/en/latest/naming.html#units - - version: - - https://cwms-database.readthedocs.io/en/latest/naming.html#versions - - version-date - - version-mask - - -Reference values for shared parameters across TimeSeries endpoints for ease of access: - - :ref:`def-end` - :ref:`def-location-id` - :ref:`def-location-mask` - :ref:`def-office` - :ref:`def-office-mask` - :ref:`def-page` - :ref:`def-page-size` - :ref:`def-parameter-id` - :ref:`def-parameter-id-mask` - :ref:`def-timezone` - :ref:`def-unit` - :ref:`def-version-date` +This page serves as a reference for all unique parameters used across the TimeSeries endpoints. +It may be useful only for building the documentation and then be removed later. + +NON SHARED PARAMETER DEFINITIONS ARE DOCUMENTED BELOW: + +- category-id + - The text identifier for the time series category defined in the CWMS database for a specific time series. + +- datum + - The standardized reference system used for either vertical measurements. + Examples: NAVD88, NGVD29, LOCAL, etc. + +- end-time-inclusive + - Whether the resulting data set should include data occurring at the moment of the end of the time window. + Acceptable values are 'true' or 'false'. + +- format + - The desired response format. Usage differs between endpoints. See the Legacy Format Responses documentation + page for more information. + + .. note:: + Detailed documentation for Legacy Format Responses in CDA is currently in development and will be + available at https://cwms-data.usace.army.mil/cwms-data/legacy-format in a future release. + +- group-id + - The text identifier of the time series group defined in the CWMS database for a specific time series. + +- include-entry-date + - Whether to include in the response for a data retrieval the timestamps at which each data point was entered + into the CWMS database. Acceptable values are 'true' or 'false'. + +- max-version + - Whether to use the most recent version date in the response. Only applies to time series that utilize dates in + the version field. Acceptable values are 'true' or 'false'. + +- name + - The text representation of the unique time series identifier. + +- next + - Whether to include the next time window of the time series profile instance. + +- previous + - Whether to include the previous time window of the time series profile instance. Acceptable values are 'true' + or 'false'. + +- start-time-inclusive + - Whether the resulting data set should include data occurring at the moment of the beginning of the time window. + Acceptable values are 'true' or 'false'. + +- trim + - Specifies whether to trim missing values from the beginning and end of the retrieved values. Acceptable values + are 'true' or 'false'. +- ts-ids + - A comma separated list of timeseries identifiers to be included in the response. + Example: 'Location.Elev.Inst.0.1Day.lrgs,Location2.Elev.Inst.0.12Hour.lrgs'. + `CWMS database - time series definition `_ + +- unit-system + - The unit system to convert the response data into. Available unit systems are 'SI' or 'EN'. + +- units: + - https://cwms-database.readthedocs.io/en/latest/naming.html#units + +- version: + - https://cwms-database.readthedocs.io/en/latest/naming.html#versions + +- version-mask + - A regular expression used to filter the version field for time series retrieval. + See the Regex documentation for more information on usage. + + .. note:: + Detailed documentation for Regex usage in CDA is currently in development and will be available at + https://cwms-data.usace.army.mil/cwms-data/regexp in a future release. + + Notes on duplicates/variations across endpoints: -- end and end-time-inclusive: -- begin and start and start-time-inclusive -- unit and units and unit-system: Not sure why all three exist, but here is how they are used: +- unit and units and unit-system: here is how they are used: - unit: deprecated, prefer units, SI or EN or other - units: SI or EN or other @@ -139,8 +157,7 @@ version and version-date and version-mask: /timeSeries/profile-instance{location-id}/{parameter-id}/{version}" "trim","/timeSeries; /timeSeries/profile" - "ts-ids","/timeSeries/recent; - /timeSeries/profile-parser" + "ts-ids","/timeSeries/recent" "unit","/timeSeries; /timeSeries/profile-instance; /timeSeries/profile-instance{location-id}/{parameter-id}/{version}" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e49a183ea..5b2019c41f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,9 +9,9 @@ hec-nucleus = "2.0.1" flogger = "0.7.4" google-findbugs = "3.0.2" error_prone_annotations = "2.15.0" -cwms-ratings = "2.0.2" +cwms-ratings = "4.2.3" javalin = "4.6.8" -tomcat = "9.0.112" +tomcat = "9.0.115" swagger-core = "2.2.23" jackson = "2.17.1" geojson-jackson = "1.14" @@ -36,6 +36,8 @@ freemarker = "2.3.32" auto-service = "1.1.1" openapi-validation = "2.44.9" javaparser = "3.26.2" +togglz = "3.3.3" +minio = "8.6.0" #Overrides classgraph = { strictly = '4.8.176' } @@ -79,7 +81,9 @@ jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-dat jackson-dataformat-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" } jackson-datatype-jdk8 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", version.ref = "jackson" } +togglz-core = { module = "org.togglz:togglz-core", version.ref = "togglz" } +minio = { module = "io.minio:minio", version.ref = "minio"} #compile compileOnly javaee-web-api = { module = "javax:javaee-web-api", version.ref = "java-ee" } @@ -106,6 +110,8 @@ testcontainers-database-commons = { module = "org.testcontainers:testcontainers- testcontainers-jdbc = { module = "org.testcontainers:testcontainers-jdbc", version.ref = "testcontainers"} testcontainers-junit-jupiter = { module = "org.testcontainers:testcontainers-junit-jupiter", version.ref = "testcontainers"} testcontainers-cwms = { module = "mil.army.usace.hec:testcontainers-cwms", version.ref = "cwms-testcontainers"} +testcontainers-minio = { module = "org.testcontainers:testcontainers-minio", version.ref = "testcontainers" } + mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } oracle-jdbc-driver = { module ="com.oracle.database.jdbc:ojdbc8", version.ref = "oracle-jdbc" } rest-assured = { module = "io.rest-assured:rest-assured", version.ref = "rest-assured" } @@ -137,7 +143,7 @@ junit = ["junit-jupiter-api", "junit-jupiter-params", "junit-jupiter-engine", "j tomcat-embedded = [ "tomcat-embedded-core", "tomcat-embedded-jasper" ] tomcat-support = [ "tomcat-juli", "tomcat-jdbc" ] testcontainers = [ "testcontainers-base", "testcontainers-database-commons", "testcontainers-jdbc", - "testcontainers-junit-jupiter", "testcontainers-cwms"] + "testcontainers-junit-jupiter", "testcontainers-cwms", "testcontainers-minio" ] metrics = ["metrics-core", "metrics-servlets", "metrics-prometheus-client", "metrics-prometheus-servlets" ] jackson = ["jackson-core", "jackson-dataformat-csv", "jackson-dataformat-xml", "jackson-datatype-jsr310" ] overrides = ["io-github.classgraph", "io-swagger-parser"] diff --git a/load_data/.ipynb_checkpoints/dev_data_copy-checkpoint.ipynb b/load_data/.ipynb_checkpoints/dev_data_copy-checkpoint.ipynb new file mode 100644 index 0000000000..24629d8f6a --- /dev/null +++ b/load_data/.ipynb_checkpoints/dev_data_copy-checkpoint.ipynb @@ -0,0 +1,590 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "64426a8a-c361-4e80-986d-451bf08907a5", + "metadata": {}, + "outputs": [], + "source": [ + "#libraries\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "import sys\n", + "sys.path.insert(0, \"C:/Soft/repos/cwms-python\")\n", + "import cwms\n", + "import numpy as np\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "63849890-9155-4c2b-8a62-96aa9fbe0de4", + "metadata": {}, + "source": [ + "### Initialize system Load Location IDs" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a5d7f8c6-8b5d-44ec-95d1-1e4ea388d732", + "metadata": {}, + "outputs": [], + "source": [ + "office_id = 'LRL'\n", + "start_date = pd.to_datetime(\"3/1/2025\").tz_localize('UTC')\n", + "end_date = pd.to_datetime(\"6/13/2025\").tz_localize('UTC')\n", + "office_id_lower = office_id.lower() \n", + "apiRoot_src = f\"https://wm.{office_id_lower}.ds.usace.army.mil:8243/{office_id_lower}-data/\"\n", + "location_id_file = 'base_locations_to_grab.csv'\n", + "api = cwms.api.init_session(api_root=apiRoot_src)\n", + "grab_locs = pd.read_csv(location_id_file)" + ] + }, + { + "cell_type": "markdown", + "id": "7da9432f-c362-4d22-a981-9d8fb26cb593", + "metadata": {}, + "source": [ + "### Grab all locations and the get information for subset" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9cc1f27-c358-49e3-b14d-751599feb8e3", + "metadata": {}, + "outputs": [], + "source": [ + "location = cwms.get_locations_catalog(office_id=office_id).df\n", + "grab_locs_office = grab_locs[grab_locs['Office']==office_id]\n", + "pattern = \"|\".join(grab_locs_office['Base_Location'])\n", + "public_locations = location[location['name'].str.contains(pattern)]\n", + "public_locations.to_csv(f'data/{office_id}_locations_data.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "962aac10-218b-4cab-af88-013eab522cc0", + "metadata": {}, + "source": [ + "### Collect All Timeseries ID assigned to the Locations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43985fba-3cf4-4e71-bc7b-efd997e4c18c", + "metadata": {}, + "outputs": [], + "source": [ + "final_tsids =[]\n", + "for loc in public_locations['name']:\n", + " loc_repex = loc+'.*'\n", + " cat_ts = cwms.get_timeseries_catalog(office_id=office_id,like=loc_repex,include_extents=True,timeseries_group_like=None).df\n", + " for index, ts in cat_ts.iterrows():\n", + "\n", + " extents = pd.DataFrame(ts['extents'])\n", + " if 'latest-time' in extents.columns:\n", + " if len(extents) == 1 and (pd.to_datetime(extents['latest-time'].iloc[0]) > start_date):\n", + "\n", + " final_tsids.append(ts['name'])\n", + " if len(extents) > 1:\n", + "\n", + " extents['version-time'] = pd.to_datetime(extents['version-time'])\n", + " extents = extents[extents['version-time'] > start_date]\n", + " if len(extents) > 0:\n", + " num = min([len(extents),5])\n", + " extents_sorted = extents.sort_values(by='version-time',ascending=False)\n", + " extents_tosave = extents_sorted.iloc[0:num]\n", + " for version in extents_tosave['version-time']:\n", + " final_tsids.append(f'{ts[\"name\"]}:{version}')\n", + "ts_ids = pd.DataFrame({'office':office_id,'ts_id':final_tsids}) \n", + "ts_ids = ts_ids.drop_duplicates()\n", + "ts_ids.to_csv(f'data/{office_id}_timeseries_ids.csv')" + ] + }, + { + "cell_type": "markdown", + "id": "b3b85b3f-89f1-4987-a890-0890e9fa7b2f", + "metadata": {}, + "source": [ + "### Grab Timeseries Values for All Timeseries IDS" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "ecae8f73-3ab7-4f04-9226-4325e72ad60a", + "metadata": {}, + "outputs": [], + "source": [ + "ts_ids = pd.read_csv(f'data/{office_id}_timeseries_ids.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "46671d37-b440-4219-9f78-9d4042d95ffd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 59.33675069999998 seconds\n" + ] + } + ], + "source": [ + "start_time = time.perf_counter()\n", + "multi_ts_melt = cwms.get_multi_timeseries_df(ts_ids=ts_ids['ts_id'],office_id=office_id,melted=True,begin=start_date,end=end_date)\n", + "end_time = time.perf_counter()\n", + "elapsed_time = end_time - start_time\n", + "print(f\"Elapsed time: {elapsed_time} seconds\")\n", + "multi_ts_melt.to_parquet(f'data/{office_id}_timeseries_values_melted.parquet')" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "29caaf2f-842e-40d3-9f37-5095512cdd0e", + "metadata": {}, + "outputs": [], + "source": [ + "multi_ts_melt = pd.read_parquet('data/MVP_timeseries_values_melted.parquet')\n", + "apiRoot_dev = \"https://water.dev.cwbi.us/cwms-data/\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "163df7d8-6e6a-4612-a022-6af40427f6a8", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + " ········\n" + ] + } + ], + "source": [ + "from getpass import getpass\n", + "apiKey = getpass()\n", + "apiKey_dev = \"apikey \" + apiKey" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5a2cef41-20e3-45bd-9de2-4887727b9531", + "metadata": {}, + "outputs": [], + "source": [ + "api = cwms.api.init_session(api_root=apiRoot_dev, api_key=apiKey_dev)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d2cf42da-f5a0-4915-8a9d-4e139d6a1705", + "metadata": {}, + "outputs": [], + "source": [ + "cwms.store_multi_timeseries_df(ts_data=multi_ts_melt,office_id='MVP')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "275cdfd7-4f60-4731-bf1f-24f4129876f0", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'start_date' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[10], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtime\u001b[39;00m\n\u001b[0;32m 2\u001b[0m start_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mperf_counter()\n\u001b[1;32m----> 3\u001b[0m multi_ts_melt_dev \u001b[38;5;241m=\u001b[39m cwms\u001b[38;5;241m.\u001b[39mget_multi_timeseries_df(ts_ids\u001b[38;5;241m=\u001b[39mfinal_tsids,office_id\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mMVP\u001b[39m\u001b[38;5;124m'\u001b[39m,melted\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,begin\u001b[38;5;241m=\u001b[39m\u001b[43mstart_date\u001b[49m,end\u001b[38;5;241m=\u001b[39mend_date)\n\u001b[0;32m 4\u001b[0m end_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mperf_counter()\n\u001b[0;32m 5\u001b[0m elapsed_time \u001b[38;5;241m=\u001b[39m end_time \u001b[38;5;241m-\u001b[39m start_time\n", + "\u001b[1;31mNameError\u001b[0m: name 'start_date' is not defined" + ] + } + ], + "source": [ + "import time\n", + "start_time = time.perf_counter()\n", + "multi_ts_melt_dev = cwms.get_multi_timeseries_df(ts_ids=final_tsids,office_id='MVP',melted=True,begin=start_date,end=end_date)\n", + "end_time = time.perf_counter()\n", + "elapsed_time = end_time - start_time\n", + "print(f\"Elapsed time: {elapsed_time} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4dd0de1-9413-41e3-a956-b652865de0ea", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d408b30-61f1-4e29-9ef4-56be7298431d", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "584e2089-990c-4387-969a-a0652bb4b681", + "metadata": {}, + "outputs": [], + "source": [ + "unique_tsids = (multi_ts_melt['ts_id'].astype(str) + ':' + multi_ts_melt['version_date'].astype(str)).unique()" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "3ab1f866-ce7f-4a7b-be11-d2351abe4830", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp:NaT',\n", + " 'LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry:NaT',\n", + " 'LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry:NaT',\n", + " ..., 'LockDam_02.Temp-Water.Inst.15Minutes.0.merged:NaT',\n", + " 'LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM:NaT',\n", + " 'LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw:NaT'], dtype=object)" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unique_tsids" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "fcfeb20c-7f10-4847-af78-1fd56d99e40a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cfs'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data['units'].iloc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "434965dc-caf8-44ba-a266-e8ca609bcfe4", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed5b113d-e63a-4a8a-a808-521dd7a09805", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d26f19f-1319-477c-852b-363f158980c7", + "metadata": {}, + "outputs": [], + "source": [ + "data = multi_ts_melt.query('ts_id' == 'LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp' and 'version_date' < 9')" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "4f5afd19-4240-4818-bf2d-68f2119a2d6a", + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Index contains duplicate entries, cannot reshape", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[45], line 11\u001b[0m\n\u001b[0;32m 7\u001b[0m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m (\n\u001b[0;32m 8\u001b[0m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mstr[:\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m2\u001b[39m] \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m:\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mstr[\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m2\u001b[39m:]\n\u001b[0;32m 9\u001b[0m )\n\u001b[0;32m 10\u001b[0m multi_ts\u001b[38;5;241m.\u001b[39mfillna({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m}, inplace\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m---> 11\u001b[0m multi_ts_unmelt \u001b[38;5;241m=\u001b[39m \u001b[43mmulti_ts\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mdate-time\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcols\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalues\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mvalue\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\frame.py:9025\u001b[0m, in \u001b[0;36mDataFrame.pivot\u001b[1;34m(self, columns, index, values)\u001b[0m\n\u001b[0;32m 9018\u001b[0m \u001b[38;5;129m@Substitution\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 9019\u001b[0m \u001b[38;5;129m@Appender\u001b[39m(_shared_docs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpivot\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[0;32m 9020\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mpivot\u001b[39m(\n\u001b[0;32m 9021\u001b[0m \u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m, columns, index\u001b[38;5;241m=\u001b[39mlib\u001b[38;5;241m.\u001b[39mno_default, values\u001b[38;5;241m=\u001b[39mlib\u001b[38;5;241m.\u001b[39mno_default\n\u001b[0;32m 9022\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DataFrame:\n\u001b[0;32m 9023\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpivot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m pivot\n\u001b[1;32m-> 9025\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalues\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\pivot.py:553\u001b[0m, in \u001b[0;36mpivot\u001b[1;34m(data, columns, index, values)\u001b[0m\n\u001b[0;32m 549\u001b[0m indexed \u001b[38;5;241m=\u001b[39m data\u001b[38;5;241m.\u001b[39m_constructor_sliced(data[values]\u001b[38;5;241m.\u001b[39m_values, index\u001b[38;5;241m=\u001b[39mmultiindex)\n\u001b[0;32m 550\u001b[0m \u001b[38;5;66;03m# error: Argument 1 to \"unstack\" of \"DataFrame\" has incompatible type \"Union\u001b[39;00m\n\u001b[0;32m 551\u001b[0m \u001b[38;5;66;03m# [List[Any], ExtensionArray, ndarray[Any, Any], Index, Series]\"; expected\u001b[39;00m\n\u001b[0;32m 552\u001b[0m \u001b[38;5;66;03m# \"Hashable\"\u001b[39;00m\n\u001b[1;32m--> 553\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mindexed\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcolumns_listlike\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n\u001b[0;32m 554\u001b[0m result\u001b[38;5;241m.\u001b[39mindex\u001b[38;5;241m.\u001b[39mnames \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m 555\u001b[0m name \u001b[38;5;28;01mif\u001b[39;00m name \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m lib\u001b[38;5;241m.\u001b[39mno_default \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01mfor\u001b[39;00m name \u001b[38;5;129;01min\u001b[39;00m result\u001b[38;5;241m.\u001b[39mindex\u001b[38;5;241m.\u001b[39mnames\n\u001b[0;32m 556\u001b[0m ]\n\u001b[0;32m 558\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\series.py:4455\u001b[0m, in \u001b[0;36mSeries.unstack\u001b[1;34m(self, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 4410\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4411\u001b[0m \u001b[38;5;124;03mUnstack, also known as pivot, Series with MultiIndex to produce DataFrame.\u001b[39;00m\n\u001b[0;32m 4412\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 4451\u001b[0m \u001b[38;5;124;03mb 2 4\u001b[39;00m\n\u001b[0;32m 4452\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4453\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m unstack\n\u001b[1;32m-> 4455\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:494\u001b[0m, in \u001b[0;36munstack\u001b[1;34m(obj, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 490\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(level, (\u001b[38;5;28mtuple\u001b[39m, \u001b[38;5;28mlist\u001b[39m)):\n\u001b[0;32m 491\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(level) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[0;32m 492\u001b[0m \u001b[38;5;66;03m# _unstack_multiple only handles MultiIndexes,\u001b[39;00m\n\u001b[0;32m 493\u001b[0m \u001b[38;5;66;03m# and isn't needed for a single level\u001b[39;00m\n\u001b[1;32m--> 494\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_unstack_multiple\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 495\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 496\u001b[0m level \u001b[38;5;241m=\u001b[39m level[\u001b[38;5;241m0\u001b[39m]\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:445\u001b[0m, in \u001b[0;36m_unstack_multiple\u001b[1;34m(data, clocs, fill_value, sort)\u001b[0m\n\u001b[0;32m 442\u001b[0m dummy \u001b[38;5;241m=\u001b[39m data\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[0;32m 443\u001b[0m dummy\u001b[38;5;241m.\u001b[39mindex \u001b[38;5;241m=\u001b[39m dummy_index\n\u001b[1;32m--> 445\u001b[0m unstacked \u001b[38;5;241m=\u001b[39m \u001b[43mdummy\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m__placeholder__\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 446\u001b[0m new_levels \u001b[38;5;241m=\u001b[39m clevels\n\u001b[0;32m 447\u001b[0m new_names \u001b[38;5;241m=\u001b[39m cnames\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\series.py:4455\u001b[0m, in \u001b[0;36mSeries.unstack\u001b[1;34m(self, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 4410\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4411\u001b[0m \u001b[38;5;124;03mUnstack, also known as pivot, Series with MultiIndex to produce DataFrame.\u001b[39;00m\n\u001b[0;32m 4412\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 4451\u001b[0m \u001b[38;5;124;03mb 2 4\u001b[39;00m\n\u001b[0;32m 4452\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4453\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m unstack\n\u001b[1;32m-> 4455\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:517\u001b[0m, in \u001b[0;36munstack\u001b[1;34m(obj, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 515\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_1d_only_ea_dtype(obj\u001b[38;5;241m.\u001b[39mdtype):\n\u001b[0;32m 516\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _unstack_extension_series(obj, level, fill_value, sort\u001b[38;5;241m=\u001b[39msort)\n\u001b[1;32m--> 517\u001b[0m unstacker \u001b[38;5;241m=\u001b[39m \u001b[43m_Unstacker\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 518\u001b[0m \u001b[43m \u001b[49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconstructor\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_constructor_expanddim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\n\u001b[0;32m 519\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 520\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m unstacker\u001b[38;5;241m.\u001b[39mget_result(\n\u001b[0;32m 521\u001b[0m obj\u001b[38;5;241m.\u001b[39m_values, value_columns\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, fill_value\u001b[38;5;241m=\u001b[39mfill_value\n\u001b[0;32m 522\u001b[0m )\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:154\u001b[0m, in \u001b[0;36m_Unstacker.__init__\u001b[1;34m(self, index, level, constructor, sort)\u001b[0m\n\u001b[0;32m 146\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m num_cells \u001b[38;5;241m>\u001b[39m np\u001b[38;5;241m.\u001b[39miinfo(np\u001b[38;5;241m.\u001b[39mint32)\u001b[38;5;241m.\u001b[39mmax:\n\u001b[0;32m 147\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[0;32m 148\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe following operation may generate \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnum_cells\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m cells \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 149\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124min the resulting pandas object.\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 150\u001b[0m PerformanceWarning,\n\u001b[0;32m 151\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39mfind_stack_level(),\n\u001b[0;32m 152\u001b[0m )\n\u001b[1;32m--> 154\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_make_selectors\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:210\u001b[0m, in \u001b[0;36m_Unstacker._make_selectors\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 207\u001b[0m mask\u001b[38;5;241m.\u001b[39mput(selector, \u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m 209\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m mask\u001b[38;5;241m.\u001b[39msum() \u001b[38;5;241m<\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mindex):\n\u001b[1;32m--> 210\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIndex contains duplicate entries, cannot reshape\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 212\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgroup_index \u001b[38;5;241m=\u001b[39m comp_index\n\u001b[0;32m 213\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmask \u001b[38;5;241m=\u001b[39m mask\n", + "\u001b[1;31mValueError\u001b[0m: Index contains duplicate entries, cannot reshape" + ] + } + ], + "source": [ + "cols = [\"ts_id\", \"units\"]\n", + "if \"version_date\" in multi_ts.columns:\n", + " cols.append(\"version_date\")\n", + " multi_ts[\"version_date\"] = multi_ts[\"version_date\"].dt.strftime(\n", + " \"%Y-%m-%d %H:%M:%S%z\"\n", + " )\n", + " multi_ts[\"version_date\"] = (\n", + " multi_ts[\"version_date\"].str[:-2] + \":\" + multi_ts[\"version_date\"].str[-2:]\n", + " )\n", + " multi_ts.fillna({\"version_date\": \"\"}, inplace=True)\n", + "multi_ts_unmelt = multi_ts.pivot(index=\"date-time\", columns=cols, values=\"value\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b4c37c8-7a23-466e-804b-1ace3757e68c", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da6a9982-318b-46a0-80b1-3db1c5d51235", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a88d03ff-d144-4219-847c-8c65fbe14ed1", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3e4e1368-a2c4-4483-ac2c-695110e8a02e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date-timevaluequality-codets_idunits
02025-03-01 00:00:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
12025-03-01 00:15:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
22025-03-01 00:30:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
32025-03-01 00:45:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
42025-03-01 01:00:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
..................
41293502025-06-06 10:30:00+00:0069.40LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293512025-06-07 10:00:00+00:0069.30LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293522025-06-09 10:00:00+00:0067.00LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293532025-06-11 10:00:00+00:0068.80LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293542025-06-12 10:00:00+00:0067.60LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
\n", + "

4129355 rows × 5 columns

\n", + "
" + ], + "text/plain": [ + " date-time value quality-code \\\n", + "0 2025-03-01 00:00:00+00:00 0.0 0 \n", + "1 2025-03-01 00:15:00+00:00 0.0 0 \n", + "2 2025-03-01 00:30:00+00:00 0.0 0 \n", + "3 2025-03-01 00:45:00+00:00 0.0 0 \n", + "4 2025-03-01 01:00:00+00:00 0.0 0 \n", + "... ... ... ... \n", + "4129350 2025-06-06 10:30:00+00:00 69.4 0 \n", + "4129351 2025-06-07 10:00:00+00:00 69.3 0 \n", + "4129352 2025-06-09 10:00:00+00:00 67.0 0 \n", + "4129353 2025-06-11 10:00:00+00:00 68.8 0 \n", + "4129354 2025-06-12 10:00:00+00:00 67.6 0 \n", + "\n", + " ts_id units \n", + "0 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "1 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "2 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "3 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "4 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "... ... ... \n", + "4129350 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129351 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129352 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129353 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129354 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "\n", + "[4129355 rows x 5 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "\n", + "multi_ts_melt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fcf78f9-5007-4440-bd4e-0b08fda5f812", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/load_data/.ipynb_checkpoints/dev_data_load-checkpoint.ipynb b/load_data/.ipynb_checkpoints/dev_data_load-checkpoint.ipynb new file mode 100644 index 0000000000..7837dcb364 --- /dev/null +++ b/load_data/.ipynb_checkpoints/dev_data_load-checkpoint.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "fc24b3e1-2a70-436a-a8e3-0bf0e8a9195f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "import sys\n", + "import cwms\n", + "import numpy as np\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "978352f0-352e-4303-ae23-93838a17e704", + "metadata": {}, + "source": [ + "### Initialize CDA root and key" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb32f1d3-62a1-447a-b7fe-d0b48f3ccf01", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "office_ids = ['LRL','MVP']\n", + "apiRoot_dev = \"http://localhost:8081/cwms-data/\"\n", + "apiKey_dev = \"apikey testkey12345677\"\n", + "api = cwms.api.init_session(api_root=apiRoot_dev, api_key=apiKey_dev)" + ] + }, + { + "cell_type": "markdown", + "id": "7bd62514-550c-4f3f-a907-52502b10d3ff", + "metadata": {}, + "source": [ + "### Store Locations to Database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04001bfd-ef62-4233-a249-cb9cbcba1042", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def default_val(value, default):\n", + " if pd.isna(value) or value == 'Unknown or Not Applicable':\n", + " value = default\n", + " return value\n", + "\n", + "def store_multi_location_df(locations):\n", + " for i,row in locations.iterrows():\n", + " if row['active']:\n", + " loc_json = {\n", + " \"office-id\": row['office'], # required\n", + " \"name\": row['name'], #required\n", + " \"latitude\": float(default_val(row['latitude'],'38.0')), #required\n", + " \"longitude\": float(default_val(row['longitude'],'-85.0')), #required\n", + " \"active\": row['active'], #required\n", + " \"public-name\": default_val(row['public-name'],row['name']),\n", + " \"long-name\": row['long-name'],\n", + " \"description\": row['description'],\n", + " \"timezone-name\": default_val(row['time-zone'],'US/Eastern'), #required\n", + " \"location-type\": row['type'], \n", + " \"location-kind\": row['kind'], #required\n", + " \"nation\": 'US', #required and abbreviated\n", + " #\"state-initial\": row['state'], #Saving state doesn't work.\n", + " #\"county-name\": row['county'],\n", + " \"nearest-city\": row['nearest-city'],\n", + " \"horizontal-datum\": default_val(row['horizontal-datum'],'NAD27'), #required\n", + " #\"published-longitude\": float(row['published-longitude']),\n", + " #\"published-latitude\": float(row['published-latitude']),\n", + " \"vertical-datum\": row['vertical-datum'],\n", + " \"elevation\": float(row['elevation']),\n", + " \"map-label\": row['map-label'],\n", + " \"bounding-office-id\": row['bounding-office'],\n", + " \"elevation-units\": row['unit']\n", + " }\n", + " clean_dict = {k: loc_json[k] for k in loc_json if not pd.isna(loc_json[k])}\n", + " #try:\n", + " cwms.store_location(data = clean_dict)\n", + " #except:\n", + " # print(clean_dict)\n", + " # print('save failed')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6c56107-9c10-4c52-b8b3-2bf91e1d01ea", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for office_id in office_ids:\n", + " locations = pd.read_csv(f'data/{office_id}_locations_data.csv')\n", + " store_multi_location_df(locations)" + ] + }, + { + "cell_type": "markdown", + "id": "ec280dcb-a4e3-4a58-af91-c16aa1d88441", + "metadata": {}, + "source": [ + "#### Check if Locations are present" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48b25256-f248-4247-ae03-f86e3b961584", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check = cwms.get_locations_catalog(office_id='LRL').df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9130c6f-b28c-4fba-be63-d9500787664b", + "metadata": {}, + "outputs": [], + "source": [ + "location_check" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b1c2c8a-ff32-4087-a690-6ae32c0ff8f1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check = cwms.get_locations_catalog(office_id='MVP').df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41aac268-fbec-490f-aaa1-471d975fcd43", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check" + ] + }, + { + "cell_type": "markdown", + "id": "105169d5-4216-4429-8ffc-a356a0aa99a9", + "metadata": {}, + "source": [ + "### Store Timeseries Values To Database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e3e0a05-ef0e-450d-8b10-d5f8a8948463", + "metadata": {}, + "outputs": [], + "source": [ + "for office_id in office_ids:\n", + " print(f'importing timeseries for {office_id}')\n", + " ts_ids = pd.read_csv(f'data/{office_id}_timeseries_ids_used.csv')\n", + " multi_ts_melt = pd.read_parquet(f'data/{office_id}_timeseries_values_melted.parquet')\n", + " multi_ts_melt = multi_ts_melt.dropna(subset=['value'])\n", + " multi_ts_melt_used = multi_ts_melt[multi_ts_melt['ts_id'].isin(ts_ids['ts_id'])]\n", + " cwms.store_multi_timeseries_df(data=multi_ts_melt_used,office_id=office_id,max_workers=10)" + ] + }, + { + "cell_type": "markdown", + "id": "631f8aee-aa70-41cf-bfaf-d75f36b7d95b", + "metadata": {}, + "source": [ + "#### Check Timeseries Values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d0da1e2-9b1c-4655-aa8f-b377181b4508", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ts_ids = cwms.get_timeseries_catalog(office_id='MVP',timeseries_group_like=None,page_size=10000,include_extents=True).df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3b50dd7-c0dd-4d1c-8a94-9adf904f93b0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ts_ids" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcf0c940-97e9-48e9-9fd1-e73d92b86c0f", + "metadata": {}, + "outputs": [], + "source": [ + "start_date = pd.to_datetime('03/01/2025').tz_localize('UTC')\n", + "end_date = pd.to_datetime('06/13/2025').tz_localize('UTC')\n", + "data = cwms.get_timeseries(ts_id='ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS',office_id='MVP',begin=start_date,end=end_date)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57cdb085-a27a-4814-b834-827728474f0e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data.df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8926cbfa-6029-415f-bc13-29c9c4a3bb21", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/load_data/.ipynb_checkpoints/test-checkpoint.csv b/load_data/.ipynb_checkpoints/test-checkpoint.csv new file mode 100644 index 0000000000..8734fe9efe --- /dev/null +++ b/load_data/.ipynb_checkpoints/test-checkpoint.csv @@ -0,0 +1,1241 @@ +,office,name,units,interval,interval-offset,time-zone,extents +0,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:16.063259Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:17.081731Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:16.0351Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:15.841988Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:14.655117Z'}]" +1,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:17.12Z'}]" +2,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:16.174Z'}]" +3,MVP,ABRN8.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-22T05:00:00Z', 'last-update': '2025-06-16T18:43:17.214247Z'}]" +4,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:18.586Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:18.351288Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:19.609036Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:18.338475Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:17.268529Z'}]" +5,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:18.511Z'}]" +6,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:18.689Z'}]" +7,MVP,ABRN8.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-09T03:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:19.633Z'}]" +8,MVP,ABRN8.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-09T03:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:19.73Z'}]" +9,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:22.379599Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:20.954053Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:20.915176Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:22.192557Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:19.884108Z'}]" +10,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:20.994Z'}]" +11,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:21.256Z'}]" +12,MVP,ABRN8.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-22T05:00:00Z', 'last-update': '2025-06-16T18:43:22.302515Z'}]" +13,MVP,ABRN8.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.534Z'}]" +14,MVP,ABRN8.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.433Z'}]" +15,MVP,AGYM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-04-11T05:00:00Z', 'latest-time': '2025-05-02T05:00:00Z', 'last-update': '2025-06-16T18:42:48.29Z'}]" +16,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:42:52.946995Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:51.942805Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:51.891592Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:49.521024Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:49.366422Z'}]" +17,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:52.939Z'}]" +18,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:42:53.339Z'}]" +19,MVP,AGYM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-15T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:54.693Z'}]" +20,MVP,AGYM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-15T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:54.495Z'}]" +21,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:42:57.23343Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:57.221488Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:59.316883Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:58.074952Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:56.804937Z'}]" +22,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.183Z'}]" +23,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:42:58.231Z'}]" +24,MVP,AGYM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.505Z'}]" +25,MVP,AGYM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-03T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.42Z'}]" +26,MVP,Baldhill_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:01.697Z'}]" +27,MVP,Baldhill_Dam-FishPondSiphon.Flow.Inst.15Minutes.0.CEMVP-ProjectEntry,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:41.038Z'}]" +28,MVP,Baldhill_Dam-FishPondSiphon.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:40.959Z'}]" +29,MVP,Baldhill_Dam-FishPondSiphon.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:35:40.141Z'}]" +30,MVP,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:08.25Z'}]" +31,MVP,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:01.942Z'}]" +32,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:48.947Z'}]" +33,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:05.728Z'}]" +34,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:39:06.846Z'}]" +35,MVP,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:09.364Z'}]" +36,MVP,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:09.37Z'}]" +37,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:04.069762Z'}]" +38,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:10.845Z'}]" +39,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:17:06.967Z'}]" +40,MVP,Baldhill_Dam-Tailwater.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw,umho/cm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:51.209Z'}]" +41,MVP,Baldhill_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:52.704Z'}]" +42,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:52.396Z'}]" +43,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:51.445Z'}]" +44,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:54.956Z'}]" +45,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:55.039Z'}]" +46,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:56.491Z'}]" +47,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:56.542Z'}]" +48,MVP,Baldhill_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T17:57:48.426Z'}]" +49,MVP,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:57:51.998Z'}]" +50,MVP,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:57:52.095Z'}]" +51,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:35:57.662861Z'}]" +52,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:10Z', 'last-update': '2025-06-16T18:35:59.250667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:47Z', 'last-update': '2025-06-16T18:35:59.141927Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:27Z', 'last-update': '2025-06-16T18:36:00.397044Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:17Z', 'last-update': '2025-06-16T18:36:00.236065Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:48Z', 'last-update': '2025-06-16T18:35:58.876053Z'}]" +53,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:36:00.232Z'}]" +54,MVP,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:00.447Z'}]" +55,MVP,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:01.348Z'}]" +56,MVP,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:36:01.436Z'}]" +57,MVP,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T18:36:01.531Z'}]" +58,MVP,Baldhill_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:01.645Z'}]" +59,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:03.961Z'}]" +60,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:02.96Z'}]" +61,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.244Z'}]" +62,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.344Z'}]" +63,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:06.45Z'}]" +64,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-22T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.243Z'}]" +65,MVP,Baldhill_Dam-Tailwater.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:36:05.569Z'}]" +66,MVP,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-11T15:00:00Z', 'last-update': '2025-06-16T18:36:06.648Z'}]" +67,MVP,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-11T15:00:00Z', 'last-update': '2025-06-16T18:36:07.827Z'}]" +68,MVP,Baldhill_Dam-Tailwater.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:10.485Z'}]" +69,MVP,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:11.106Z'}]" +70,MVP,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:19.982796Z'}]" +71,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:12.236Z'}]" +72,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:13.54Z'}]" +73,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T17:33:01.820815Z'}]" +74,MVP,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.3Z'}]" +75,MVP,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:34.587Z'}]" +76,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:30.416Z'}]" +77,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:30.141Z'}]" +78,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T17:58:19.802Z'}]" +79,MVP,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:27.234165Z'}]" +80,MVP,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:28.520759Z'}]" +81,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:18.446Z'}]" +82,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:20.797Z'}]" +83,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:39:19.539Z'}]" +84,MVP,Baldhill_Dam.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.025Z'}]" +85,MVP,Baldhill_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:19.491887Z'}]" +86,MVP,Baldhill_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.308Z'}]" +87,MVP,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.428Z'}]" +88,MVP,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.428Z'}]" +89,MVP,Baldhill_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:21.191Z'}]" +90,MVP,Baldhill_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:21.277Z'}]" +91,MVP,Baldhill_Dam.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:22.424Z'}]" +92,MVP,Baldhill_Dam.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:22.346Z'}]" +93,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:23.454Z'}]" +94,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:23.852Z'}]" +95,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:24.675Z'}]" +96,MVP,Baldhill_Dam.Elev.Inst.1Day.0.Regulating,m,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:24.961Z'}]" +97,MVP,Baldhill_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:25.052Z'}]" +98,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:28.720575Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:28.672197Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:28.572603Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:27.444902Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:27.171473Z'}]" +99,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:08Z', 'last-update': '2025-06-16T18:39:32.500789Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:45Z', 'last-update': '2025-06-16T18:39:30.115722Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:26Z', 'last-update': '2025-06-16T18:39:32.710233Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:16Z', 'last-update': '2025-06-16T18:39:32.316786Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:46Z', 'last-update': '2025-06-16T18:39:29.873925Z'}]" +100,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:29.965Z'}]" +101,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:30.957Z'}]" +102,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:31.072Z'}]" +103,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:31.373Z'}]" +104,MVP,Baldhill_Dam.Elev.Inst.~1Day.0.Regulating,m,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T06:00:00Z', 'latest-time': '2025-04-16T06:00:00Z', 'last-update': '2025-06-16T18:39:31.061Z'}]" +105,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:33.501Z'}]" +106,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:32.577Z'}]" +107,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:34.773Z'}]" +108,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:33.751Z'}]" +109,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:33.9Z'}]" +110,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:39:33.97Z'}]" +111,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:39:35.06Z'}]" +112,MVP,Baldhill_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:39:35.164Z'}]" +113,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.093Z'}]" +114,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:39.887Z'}]" +115,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:35.068Z'}]" +116,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.455Z'}]" +117,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:37.459Z'}]" +118,MVP,Baldhill_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.465Z'}]" +119,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:38.930637Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:38.877042Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:41.318813Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:38.593465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:41.105721Z'}]" +120,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:08Z', 'last-update': '2025-06-16T18:39:41.102657Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:44Z', 'last-update': '2025-06-16T18:39:41.165453Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:25Z', 'last-update': '2025-06-16T18:39:40.174691Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:15Z', 'last-update': '2025-06-16T18:39:40.198756Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:45Z', 'last-update': '2025-06-16T18:39:39.884536Z'}]" +121,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:41.204Z'}]" +122,MVP,Baldhill_Dam.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:42.37Z'}]" +123,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:42.619833Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:43.645438Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:43.795427Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:43.961306Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:42.41772Z'}]" +124,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:07Z', 'last-update': '2025-06-16T18:39:45.160249Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:44Z', 'last-update': '2025-06-16T18:39:43.820706Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:25Z', 'last-update': '2025-06-16T18:39:43.667113Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:15Z', 'last-update': '2025-06-16T18:39:44.940015Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:45Z', 'last-update': '2025-06-16T18:39:42.675927Z'}]" +125,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:43.644Z'}]" +126,MVP,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:43.668Z'}]" +127,MVP,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:43.799Z'}]" +128,MVP,Baldhill_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.789Z'}]" +129,MVP,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.857Z'}]" +130,MVP,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.927Z'}]" +131,MVP,Baldhill_Dam.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.969Z'}]" +132,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:46.385138Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:46.228256Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:45.216667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:47.476895Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:44.990174Z'}]" +133,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:09Z', 'last-update': '2025-06-16T18:39:46.43455Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:46Z', 'last-update': '2025-06-16T18:39:46.415155Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:27Z', 'last-update': '2025-06-16T18:39:46.38542Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:16Z', 'last-update': '2025-06-16T18:39:48.765305Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:47Z', 'last-update': '2025-06-16T18:39:46.245949Z'}]" +134,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:46.539Z'}]" +135,MVP,Baldhill_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.485Z'}]" +136,MVP,Baldhill_Dam.Flow.Inst.15Minutes.0.comp-gates,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.762Z'}]" +137,MVP,Baldhill_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.498Z'}]" +138,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:48.899142Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:48.793503Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:50.190682Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:50.030982Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:47.688705Z'}]" +139,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:01Z', 'last-update': '2025-06-16T18:39:50.206239Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:37Z', 'last-update': '2025-06-16T18:39:50.107133Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:18Z', 'last-update': '2025-06-16T18:39:51.534919Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:09Z', 'last-update': '2025-06-16T18:39:51.266095Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:37Z', 'last-update': '2025-06-16T18:39:49.126393Z'}]" +140,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:50.332Z'}]" +141,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:51.625464Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:51.519688Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:53.894761Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:54.05019Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:51.375083Z'}]" +142,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:24Z', 'last-update': '2025-06-16T18:39:52.836741Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:01Z', 'last-update': '2025-06-16T18:39:52.694102Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:42Z', 'last-update': '2025-06-16T18:39:55.374027Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:30Z', 'last-update': '2025-06-16T18:39:55.099601Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:04Z', 'last-update': '2025-06-16T18:39:52.557646Z'}]" +143,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:52.579Z'}]" +144,MVP,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.897Z'}]" +145,MVP,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.709Z'}]" +146,MVP,Baldhill_Dam.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.788Z'}]" +147,MVP,Baldhill_Dam.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.696Z'}]" +148,MVP,Baldhill_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-05-10T13:00:00Z', 'latest-time': '2025-05-25T13:00:00Z', 'last-update': '2025-06-16T18:39:52.83Z'}]" +149,MVP,Baldhill_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:53.893Z'}]" +150,MVP,Baldhill_Dam.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:54.224Z'}]" +151,MVP,Baldhill_Dam.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-26T03:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:54.089Z'}]" +152,MVP,Baldhill_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:53.946Z'}]" +153,MVP,Baldhill_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.182Z'}]" +154,MVP,Baldhill_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.503Z'}]" +155,MVP,Baldhill_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.518Z'}]" +156,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.503Z'}]" +157,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.764Z'}]" +158,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.012Z'}]" +159,MVP,Baldhill_Dam.Stor.Ave.15Minutes.2Hours.comp,m3,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.657Z'}]" +160,MVP,Baldhill_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:02.735Z'}]" +161,MVP,Baldhill_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:00.437Z'}]" +162,MVP,Baldhill_Dam.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:05.512Z'}]" +163,MVP,Baldhill_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:01.461Z'}]" +164,MVP,Baldhill_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:03.198Z'}]" +165,MVP,Baldhill_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:03.999Z'}]" +166,MVP,Baldhill_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T01:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.2Z'}]" +167,MVP,ChippewaDiv_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:09.118Z'}]" +168,MVP,ChippewaDiv_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T14:45:00Z', 'latest-time': '2025-05-21T17:15:00Z', 'last-update': '2025-06-16T18:40:07.942Z'}]" +169,MVP,ChippewaDiv_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:33.553Z'}]" +170,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.569Z'}]" +171,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.682Z'}]" +172,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.926Z'}]" +173,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:35.743Z'}]" +174,MVP,ChippewaDiv_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-19T05:00:00Z', 'latest-time': '2025-06-12T05:00:00Z', 'last-update': '2025-06-16T18:40:35.989875Z'}]" +175,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:36.16Z'}]" +176,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:36.960811Z'}]" +177,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T20:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.852Z'}]" +178,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T20:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.848Z'}]" +179,MVP,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:18:25.512956Z'}]" +180,MVP,ChippewaDiv_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.809Z'}]" +181,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:38.696766Z'}]" +182,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:39.774Z'}]" +183,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:41.13Z'}]" +184,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:28.189Z'}]" +185,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:30.627Z'}]" +186,MVP,ChippewaDiv_Dam-TainterGate.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:12.878Z'}]" +187,MVP,ChippewaDiv_Dam-TainterGate.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T14:45:00Z', 'latest-time': '2025-05-21T17:15:00Z', 'last-update': '2025-06-16T18:17:58.011Z'}]" +188,MVP,ChippewaDiv_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:43.374Z'}]" +189,MVP,ChippewaDiv_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:30:00Z', 'latest-time': '2025-03-17T11:30:00Z', 'last-update': '2025-06-16T18:41:42.254Z'}]" +190,MVP,ChippewaDiv_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:30:00Z', 'latest-time': '2025-03-31T11:30:00Z', 'last-update': '2025-06-16T18:41:43.14Z'}]" +191,MVP,ChippewaDiv_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-04-30T12:00:00Z', 'last-update': '2025-06-16T18:41:44.356Z'}]" +192,MVP,ChippewaDiv_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-04-30T12:00:00Z', 'last-update': '2025-06-16T18:41:43.568Z'}]" +193,MVP,ChippewaDiv_Dam.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:44.762Z'}]" +194,MVP,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.543831Z'}]" +195,MVP,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:47.172Z'}]" +196,MVP,ChippewaDiv_Dam.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:46.088Z'}]" +197,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:44.673Z'}]" +198,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-27T12:00:00Z', 'last-update': '2025-06-16T18:41:51.01957Z'}]" +199,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:41:52.443737Z'}]" +200,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-10T12:00:00Z', 'last-update': '2025-06-16T18:41:52.05Z'}]" +201,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.275Z'}]" +202,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:52.189Z'}]" +203,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:25.420226Z'}]" +204,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.286Z'}]" +205,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.48Z'}]" +206,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:57.09969Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:54.855506Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:55.956465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:54.679726Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:55.809561Z'}]" +207,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.733Z'}]" +208,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.694Z'}]" +209,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:55.818022Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:55.838514Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:57.280079Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:57.098231Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:54.928116Z'}]" +210,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:55.819Z'}]" +211,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:55.821Z'}]" +212,MVP,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:55.952Z'}]" +213,MVP,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:55.952Z'}]" +214,MVP,ChippewaDiv_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.006Z'}]" +215,MVP,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.093Z'}]" +216,MVP,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.061Z'}]" +217,MVP,ChippewaDiv_Dam.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.121Z'}]" +218,MVP,ChippewaDiv_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.298Z'}]" +219,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:57.430999Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:58.313633Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:59.741333Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:59.580737Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:57.194951Z'}]" +220,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.444Z'}]" +221,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:58.294Z'}]" +222,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:59.626113Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:58.704237Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:01.073864Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:00.844294Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:58.549139Z'}]" +223,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:58.698Z'}]" +224,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:59.776Z'}]" +225,MVP,ChippewaDiv_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:59.642Z'}]" +226,MVP,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:59.744Z'}]" +227,MVP,ChippewaDiv_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.859Z'}]" +228,MVP,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.868Z'}]" +229,MVP,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:01.013Z'}]" +230,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.904Z'}]" +231,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.214Z'}]" +232,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.219Z'}]" +233,MVP,ChippewaDiv_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:42:02.116Z'}]" +234,MVP,ChippewaDiv_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.904Z'}]" +235,MVP,ChippewaDiv_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.335Z'}]" +236,MVP,Clayton.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,America/Chicago,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:36:23.092Z'}]" +237,MVP,Clayton.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:24.294Z'}]" +238,MVP,Clayton.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:25.513Z'}]" +239,MVP,Clayton.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:42.861619Z'}]" +240,MVP,Clayton.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:26.779Z'}]" +241,MVP,Clayton.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:30.688Z'}]" +242,MVP,Clayton.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:31.791Z'}]" +243,MVP,Clayton.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.12Z'}]" +244,MVP,Clayton.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:31.91Z'}]" +245,MVP,Clayton.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.183Z'}]" +246,MVP,Clayton.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-14T14:00:00Z', 'last-update': '2025-06-16T18:36:30.949Z'}]" +247,MVP,Cooperstown.Conc-OXYGEN.Inst.15Minutes.0.CEMVP-GOES-Raw,mg/l,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-21T20:00:00Z', 'last-update': '2025-06-16T18:41:25.519Z'}]" +248,MVP,Cooperstown.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw,umho/cm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-10T15:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:21.647Z'}]" +249,MVP,Cooperstown.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:29.251Z'}]" +250,MVP,Cooperstown.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:22.959Z'}]" +251,MVP,Cooperstown.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:25.38Z'}]" +252,MVP,Cooperstown.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:05.214Z'}]" +253,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:30.692217Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:29.262443Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:31.665942Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:24.244412Z'}]" +254,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:05Z', 'last-update': '2025-06-16T18:41:34.238959Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:42Z', 'last-update': '2025-06-16T18:41:32.053423Z'}]" +255,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:01:16.671Z'}]" +256,MVP,Cooperstown.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-12T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:19:04.987Z'}]" +257,MVP,Cooperstown.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:32.063Z'}]" +258,MVP,Cooperstown.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:11.246Z'}]" +259,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:34.375644Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:34.196326Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:34.27658Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:33.2504Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:33.043256Z'}]" +260,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:06Z', 'last-update': '2025-06-16T18:41:35.483537Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:42Z', 'last-update': '2025-06-16T18:41:35.791849Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:23Z', 'last-update': '2025-06-16T18:41:34.489929Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:13Z', 'last-update': '2025-06-16T18:41:36.740441Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:43Z', 'last-update': '2025-06-16T18:41:34.282181Z'}]" +261,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:34.432Z'}]" +262,MVP,Cooperstown.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-19T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:34.57Z'}]" +263,MVP,Cooperstown.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-19T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:35.543Z'}]" +264,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:35.734828Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:35.675049Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:37.988756Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:35.470365Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:36.777554Z'}]" +265,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:27Z', 'last-update': '2025-06-16T18:41:38.2074Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:04Z', 'last-update': '2025-06-16T18:41:38.062446Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:45Z', 'last-update': '2025-06-16T18:41:39.432309Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:32Z', 'last-update': '2025-06-16T18:41:39.263304Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:07Z', 'last-update': '2025-06-16T18:41:37.005229Z'}]" +266,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:38.084Z'}]" +267,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:39.325588Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:39.28548Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:39.505689Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:40.537453Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:38.345677Z'}]" +268,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:26Z', 'last-update': '2025-06-16T18:41:40.591501Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:04Z', 'last-update': '2025-06-16T18:41:42.252535Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:44Z', 'last-update': '2025-06-16T18:41:42.097015Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:31Z', 'last-update': '2025-06-16T18:41:41.877405Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:07Z', 'last-update': '2025-06-16T18:41:39.559015Z'}]" +269,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:40.525Z'}]" +270,MVP,Cooperstown.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:41:40.538Z'}]" +271,MVP,Cooperstown.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.624Z'}]" +272,MVP,Cooperstown.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.778Z'}]" +273,MVP,Cooperstown.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.808Z'}]" +274,MVP,Cooperstown.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:41.809Z'}]" +275,MVP,Cooperstown.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-25T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.757Z'}]" +276,MVP,Cooperstown.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-10T15:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.884Z'}]" +277,MVP,Cooperstown.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:41.81Z'}]" +278,MVP,Cooperstown200.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:17:53.735Z'}]" +279,MVP,Cooperstown200.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:16.75Z'}]" +280,MVP,Cooperstown200.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:08.105Z'}]" +281,MVP,Cooperstown200.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:08.211Z'}]" +282,MVP,DAWM5.Flow.Inst.15Minutes.0.Raw-MDNR,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:24.544Z'}]" +283,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:35:25.908151Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:35:26.041937Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:35:24.766193Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:35:24.66381Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:35:24.422245Z'}]" +284,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:27.166Z'}]" +285,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:27.042Z'}]" +286,MVP,DAWM5.Stage.Inst.15Minutes.0.Raw-MDNR,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:33.38Z'}]" +287,MVP,ELZM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:40:10.318Z'}]" +288,MVP,ELZM5.Flow.Inst.0.0.Raw-USGS,cms,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T16:54:00Z', 'latest-time': '2025-05-05T19:16:00Z', 'last-update': '2025-06-16T18:40:10.829Z'}]" +289,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:40:16.947828Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:40:18.045301Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:40:15.542405Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:40:14.342368Z'}]" +290,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:00.277Z'}]" +291,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:18:00.35Z'}]" +292,MVP,ELZM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:19.206Z'}]" +293,MVP,ELZM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:19.365Z'}]" +294,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:40:21.819255Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:40:19.501898Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:40:22.075077Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:40:20.503213Z'}]" +295,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.531Z'}]" +296,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:40:20.681Z'}]" +297,MVP,ELZM5.Stage.Inst.0.0.Raw-USGS,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T16:54:00Z', 'latest-time': '2025-05-05T19:16:00Z', 'last-update': '2025-06-16T18:40:20.621Z'}]" +298,MVP,ELZM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:21.768Z'}]" +299,MVP,ELZM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-25T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.96Z'}]" +300,MVP,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.599Z'}]" +301,MVP,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.837Z'}]" +302,MVP,Highway75_Dam-LeafGate.Elev.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.8Z'}]" +303,MVP,Highway75_Dam-LeafGate.Elev.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-19T16:15:00Z', 'latest-time': '2025-05-19T16:15:00Z', 'last-update': '2025-06-16T18:32:26.948Z'}]" +304,MVP,Highway75_Dam-LeafGate.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:27.175Z'}]" +305,MVP,Highway75_Dam-LeafGate.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:28.117Z'}]" +306,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:30.688Z'}]" +307,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.03Z'}]" +308,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.209Z'}]" +309,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.046Z'}]" +310,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:38.529Z'}]" +311,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:33.202Z'}]" +312,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.364Z'}]" +313,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:03:00Z', 'latest-time': '2025-03-10T19:03:00Z', 'last-update': '2025-06-16T18:32:33.408Z'}]" +314,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:35.614Z'}]" +315,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:35.66Z'}]" +316,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.548Z'}]" +317,MVP,Highway75_Dam-LowFlow.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:39.455Z'}]" +318,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.625Z'}]" +319,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.577Z'}]" +320,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:40.687Z'}]" +321,MVP,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:44.357Z'}]" +322,MVP,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.531Z'}]" +323,MVP,Highway75_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:44.866Z'}]" +324,MVP,Highway75_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-19T16:15:00Z', 'latest-time': '2025-05-19T16:15:00Z', 'last-update': '2025-06-16T18:32:47.096Z'}]" +325,MVP,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:47.255Z'}]" +326,MVP,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.391Z'}]" +327,MVP,Highway75_Dam-LowFlow.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:49.634Z'}]" +328,MVP,Highway75_Dam-LowFlow.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.658Z'}]" +329,MVP,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:49.723Z'}]" +330,MVP,Highway75_Dam-LowFlow.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:50.029Z'}]" +331,MVP,Highway75_Dam-LowFlow.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T18:58:00Z', 'latest-time': '2025-03-10T18:58:00Z', 'last-update': '2025-06-16T18:32:49.87Z'}]" +332,MVP,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:50.034Z'}]" +333,MVP,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:51.08Z'}]" +334,MVP,Highway75_Dam-LowFlow.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:52.2Z'}]" +335,MVP,Highway75_Dam-LowFlow.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:53.64Z'}]" +336,MVP,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:57.329Z'}]" +337,MVP,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.85Z'}]" +338,MVP,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:56.248Z'}]" +339,MVP,Highway75_Dam-ServiceSpillway.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:55.987Z'}]" +340,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:27:00Z', 'latest-time': '2025-03-10T19:27:00Z', 'last-update': '2025-06-16T18:32:57.342Z'}]" +341,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.549Z'}]" +342,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.562Z'}]" +343,MVP,Highway75_Dam-ServiceSpillway.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.837Z'}]" +344,MVP,Highway75_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:01.266Z'}]" +345,MVP,Highway75_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:02.381Z'}]" +346,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:04.843056Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:03.883485Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:04.740973Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:04.972585Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:03.666933Z'}]" +347,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:04.881Z'}]" +348,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:04.958Z'}]" +349,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:05.148Z'}]" +350,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:33:05.083Z'}]" +351,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:33:06.095Z'}]" +352,MVP,Highway75_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:33:06.131Z'}]" +353,MVP,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.248Z'}]" +354,MVP,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:07.474Z'}]" +355,MVP,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.157Z'}]" +356,MVP,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.157Z'}]" +357,MVP,Highway75_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:07.251Z'}]" +358,MVP,Highway75_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:08.652Z'}]" +359,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:08.825761Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:08.814887Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:08.760374Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:11.055892Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:08.520874Z'}]" +360,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:08.862Z'}]" +361,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.792Z'}]" +362,MVP,Highway75_Dam.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.939Z'}]" +363,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:13.746319Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:12.49672Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:12.646044Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:12.369118Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:10.026781Z'}]" +364,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:10.178Z'}]" +365,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.997Z'}]" +366,MVP,Highway75_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:10.171Z'}]" +367,MVP,Highway75_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:10.209Z'}]" +368,MVP,Highway75_Dam.Flow-Out.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.141Z'}]" +369,MVP,Highway75_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.289Z'}]" +370,MVP,Highway75_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.315Z'}]" +371,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:12.337311Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:11.491397Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:13.956519Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:13.676051Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:11.279268Z'}]" +372,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.512Z'}]" +373,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.456Z'}]" +374,MVP,Highway75_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.656Z'}]" +375,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:13.905254Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:13.85237Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:13.819894Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:16.173579Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:13.647856Z'}]" +376,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:14.886Z'}]" +377,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:15.027Z'}]" +378,MVP,Highway75_Dam.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:15.165Z'}]" +379,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:17.359793Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:16.494996Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:16.314894Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:15.187683Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:17.574924Z'}]" +380,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:16.447Z'}]" +381,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.56Z'}]" +382,MVP,Highway75_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:17.369Z'}]" +383,MVP,Highway75_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.37Z'}]" +384,MVP,Highway75_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.984Z'}]" +385,MVP,Highway75_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.649Z'}]" +386,MVP,Highway75_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:17.588Z'}]" +387,MVP,Highway75_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.677Z'}]" +388,MVP,LFKM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-06-05T05:00:00Z', 'last-update': '2025-06-16T18:42:59.425Z'}]" +389,MVP,LFKM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.717Z'}]" +390,MVP,LFKM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.592Z'}]" +391,MVP,LFKM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-20T17:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.785Z'}]" +392,MVP,LFKM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-20T17:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.749Z'}]" +393,MVP,LFKM5.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:03.512Z'}]" +394,MVP,LFKM5.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:04.706Z'}]" +395,MVP,LFKM5.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:06.911Z'}]" +396,MVP,LFKM5.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:05.633Z'}]" +397,MVP,LFKM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:08.212Z'}]" +398,MVP,LFKM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:11.227Z'}]" +399,MVP,LFKM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:12.217Z'}]" +400,MVP,LFKM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:13.62Z'}]" +401,MVP,LFKM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-06-06T05:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:12.502Z'}]" +402,MVP,LFKM5.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:14.644Z'}]" +403,MVP,LFKM5.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:30:00Z', 'latest-time': '2025-06-12T23:30:00Z', 'last-update': '2025-06-16T18:43:15.972557Z'}]" +404,MVP,LockDam_02-Powerhouse.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:49.179Z'}]" +405,MVP,LockDam_02-Powerhouse.Power.Inst.15Minutes.0.CEMVP-ProjectEntry,MW,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:52.765Z'}]" +406,MVP,LockDam_02-Powerhouse.Power.Inst.~15Minutes.0.CEMVP-ProjectEntry,MW,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:06.867Z'}]" +407,MVP,LockDam_02-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:22.487Z'}]" +408,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:22.536Z'}]" +409,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.684Z'}]" +410,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:25.03Z'}]" +411,MVP,LockDam_02-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:24.844Z'}]" +412,MVP,LockDam_02-Tailwater.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:23.819Z'}]" +413,MVP,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:25.138Z'}]" +414,MVP,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.254Z'}]" +415,MVP,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:26.09Z'}]" +416,MVP,LockDam_02-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:26.147Z'}]" +417,MVP,LockDam_02-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T11:30:00Z', 'latest-time': '2025-05-28T10:15:00Z', 'last-update': '2025-06-16T18:43:26.25Z'}]" +418,MVP,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:26.444Z'}]" +419,MVP,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:32.846Z'}]" +420,MVP,LockDam_02-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.242Z'}]" +421,MVP,LockDam_02-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:28.383Z'}]" +422,MVP,LockDam_02-TainterGate01.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:33.268Z'}]" +423,MVP,LockDam_02-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.508Z'}]" +424,MVP,LockDam_02-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:30.857Z'}]" +425,MVP,LockDam_02-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.651Z'}]" +426,MVP,LockDam_02-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:32.858Z'}]" +427,MVP,LockDam_02-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:43:32.439Z'}]" +428,MVP,LockDam_02-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:53.105Z'}]" +429,MVP,LockDam_02-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:53.302Z'}]" +430,MVP,LockDam_02-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:38:54.276Z'}]" +431,MVP,LockDam_02-TainterGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:02:59.43Z'}]" +432,MVP,LockDam_02-TainterGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:03:00.732Z'}]" +433,MVP,LockDam_02-TainterGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:02:59.716Z'}]" +434,MVP,LockDam_02-TainterGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:55.625Z'}]" +435,MVP,LockDam_02-TainterGate05.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:54.6Z'}]" +436,MVP,LockDam_02-TainterGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:57.009Z'}]" +437,MVP,LockDam_02-TainterGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:38:55.695Z'}]" +438,MVP,LockDam_02-TainterGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:36.656Z'}]" +439,MVP,LockDam_02-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:38.858Z'}]" +440,MVP,LockDam_02-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:43:38.77Z'}]" +441,MVP,LockDam_02-TainterGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.413Z'}]" +442,MVP,LockDam_02-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:06.621Z'}]" +443,MVP,LockDam_02-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:04.465Z'}]" +444,MVP,LockDam_02-TainterGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.694Z'}]" +445,MVP,LockDam_02-TainterGate08.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.684Z'}]" +446,MVP,LockDam_02-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.807Z'}]" +447,MVP,LockDam_02-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:44:01.581Z'}]" +448,MVP,LockDam_02-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:06.565Z'}]" +449,MVP,LockDam_02-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:47.959Z'}]" +450,MVP,LockDam_02-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:17:47.567Z'}]" +451,MVP,LockDam_02-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:04.559Z'}]" +452,MVP,LockDam_02-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.954Z'}]" +453,MVP,LockDam_02-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:44:01.887Z'}]" +454,MVP,LockDam_02-TainterGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:09.201Z'}]" +455,MVP,LockDam_02-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:10.422Z'}]" +456,MVP,LockDam_02-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:09.314Z'}]" +457,MVP,LockDam_02-TainterGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:46.793Z'}]" +458,MVP,LockDam_02-TainterGate12.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:47.081Z'}]" +459,MVP,LockDam_02-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:46.886Z'}]" +460,MVP,LockDam_02-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:47.878Z'}]" +461,MVP,LockDam_02-TainterGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:34.978Z'}]" +462,MVP,LockDam_02-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:45.13Z'}]" +463,MVP,LockDam_02-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T17:57:38.273Z'}]" +464,MVP,LockDam_02-TainterGate14.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:49.102Z'}]" +465,MVP,LockDam_02-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:49.259Z'}]" +466,MVP,LockDam_02-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:48.107Z'}]" +467,MVP,LockDam_02-TainterGate15.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:48.724Z'}]" +468,MVP,LockDam_02-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:48.722Z'}]" +469,MVP,LockDam_02-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:13:47.717Z'}]" +470,MVP,LockDam_02-TainterGate16.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.612Z'}]" +471,MVP,LockDam_02-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.413Z'}]" +472,MVP,LockDam_02-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:58.491Z'}]" +473,MVP,LockDam_02-TainterGate17.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:11.496Z'}]" +474,MVP,LockDam_02-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:15.665Z'}]" +475,MVP,LockDam_02-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:36:12.925Z'}]" +476,MVP,LockDam_02-TainterGate18.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.515Z'}]" +477,MVP,LockDam_02-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.654Z'}]" +478,MVP,LockDam_02-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:59.413Z'}]" +479,MVP,LockDam_02-TainterGate19.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:13.158Z'}]" +480,MVP,LockDam_02-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:15.42Z'}]" +481,MVP,LockDam_02-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:14:15.25Z'}]" +482,MVP,LockDam_02-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:58.092Z'}]" +483,MVP,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:33.552Z'}]" +484,MVP,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:59.428Z'}]" +485,MVP,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:59.617Z'}]" +486,MVP,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:00.552Z'}]" +487,MVP,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:57.03Z'}]" +488,MVP,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:04.742Z'}]" +489,MVP,LockDam_02-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:59.419Z'}]" +490,MVP,LockDam_02-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:58.212Z'}]" +491,MVP,LockDam_02-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:34:57.787Z'}]" +492,MVP,LockDam_02.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T10:20:00Z', 'last-update': '2025-06-16T18:44:06.894Z'}]" +493,MVP,LockDam_02.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:02.883Z'}]" +494,MVP,LockDam_02.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:03.187Z'}]" +495,MVP,LockDam_02.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-04T11:30:00Z', 'latest-time': '2025-06-08T10:20:00Z', 'last-update': '2025-06-16T18:44:03.147Z'}]" +496,MVP,LockDam_02.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:44:03.192Z'}]" +497,MVP,LockDam_02.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:44:04.137Z'}]" +498,MVP,LockDam_02.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:05.389Z'}]" +499,MVP,LockDam_02.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:04.427Z'}]" +500,MVP,LockDam_02.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:07:51.057887Z'}]" +501,MVP,LockDam_02.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:07.898Z'}]" +502,MVP,LockDam_02.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:36:07.432Z'}]" +503,MVP,LockDam_02.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:36:07.243Z'}]" +504,MVP,LockDam_02.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:13.383Z'}]" +505,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:14.409262Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:14.680085Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:15.755201Z'}]" +506,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:07:45.865625Z'}]" +507,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T17:36:17.219Z'}]" +508,MVP,LockDam_02.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:36:17.430584Z'}]" +509,MVP,LockDam_02.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:15.805Z'}]" +510,MVP,LockDam_02.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:16.042Z'}]" +511,MVP,LockDam_02.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:15.922Z'}]" +512,MVP,LockDam_02.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.875208Z'}]" +513,MVP,LockDam_02.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:16.81Z'}]" +514,MVP,LockDam_02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:28.242111Z'}]" +515,MVP,LockDam_02.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:28.302635Z'}]" +516,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:20.611972Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:44:20.617986Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:19.487606Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:19.56505Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:18.020899Z'}]" +517,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.86421Z'}]" +518,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:44:20.741Z'}]" +519,MVP,LockDam_02.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.903Z'}]" +520,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:22.151515Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:44:22.219243Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:24.77091Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:24.428343Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:21.932781Z'}]" +521,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:23.152Z'}]" +522,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:44:23.388Z'}]" +523,MVP,LockDam_02.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:23.568Z'}]" +524,MVP,LockDam_02.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.489Z'}]" +525,MVP,LockDam_02.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:23.588Z'}]" +526,MVP,LockDam_02.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.618Z'}]" +527,MVP,LockDam_02.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T11:30:00Z', 'latest-time': '2025-05-28T10:15:00Z', 'last-update': '2025-06-16T18:44:24.761Z'}]" +528,MVP,LockDam_02.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:25.845Z'}]" +529,MVP,LockDam_02.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:27.237Z'}]" +530,MVP,LockDam_02.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:26.014Z'}]" +531,MVP,LockDam_02.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:27.021Z'}]" +532,MVP,LockDam_02.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:25.921Z'}]" +533,MVP,LockDam_02.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.233Z'}]" +534,MVP,LockDam_02.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.51Z'}]" +535,MVP,LockDam_02.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.737Z'}]" +536,MVP,LockDam_02.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:29.747Z'}]" +537,MVP,LockDam_02.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:30.939Z'}]" +538,MVP,LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T10:00:00Z', 'last-update': '2025-06-16T18:44:29.847Z'}]" +539,MVP,LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:32.231657Z'}]" +540,MVP,LockDam_05-RollerGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:48.711Z'}]" +541,MVP,LockDam_05-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:49.823Z'}]" +542,MVP,LockDam_05-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:47.427Z'}]" +543,MVP,LockDam_05-RollerGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:51.017Z'}]" +544,MVP,LockDam_05-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:52.213Z'}]" +545,MVP,LockDam_05-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:50.025Z'}]" +546,MVP,LockDam_05-RollerGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:56.021Z'}]" +547,MVP,LockDam_05-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:40.611Z'}]" +548,MVP,LockDam_05-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:56.225Z'}]" +549,MVP,LockDam_05-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:49.553708Z'}]" +550,MVP,LockDam_05-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:57.238Z'}]" +551,MVP,LockDam_05-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:58.728Z'}]" +552,MVP,LockDam_05-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:58.909Z'}]" +553,MVP,LockDam_05-RollerGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:07.67342Z'}]" +554,MVP,LockDam_05-RollerGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:59.832Z'}]" +555,MVP,LockDam_05-RollerGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:59.989Z'}]" +556,MVP,LockDam_05-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:10.200035Z'}]" +557,MVP,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:07.496429Z'}]" +558,MVP,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:58.77346Z'}]" +559,MVP,LockDam_05-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:01.427Z'}]" +560,MVP,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:02.333Z'}]" +561,MVP,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:03.95Z'}]" +562,MVP,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:03.568Z'}]" +563,MVP,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:02.795Z'}]" +564,MVP,LockDam_05-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:08.734Z'}]" +565,MVP,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.79771Z'}]" +566,MVP,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:09.954Z'}]" +567,MVP,LockDam_05-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:11.535Z'}]" +568,MVP,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:11.494Z'}]" +569,MVP,LockDam_05-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:12.446109Z'}]" +570,MVP,LockDam_05-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T15:00:00Z', 'latest-time': '2025-06-01T16:45:00Z', 'last-update': '2025-06-16T17:58:33.035452Z'}]" +571,MVP,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:54.92Z'}]" +572,MVP,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:13.745Z'}]" +573,MVP,LockDam_05-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:14.043Z'}]" +574,MVP,LockDam_05-TainterGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:19.457Z'}]" +575,MVP,LockDam_05-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:21.672Z'}]" +576,MVP,LockDam_05-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:20.762Z'}]" +577,MVP,LockDam_05-TainterGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:16.468Z'}]" +578,MVP,LockDam_05-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:16.659Z'}]" +579,MVP,LockDam_05-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:16.34Z'}]" +580,MVP,LockDam_05-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:47.288046Z'}]" +581,MVP,LockDam_05-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:58:19.848Z'}]" +582,MVP,LockDam_05-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T17:58:18.97Z'}]" +583,MVP,LockDam_05-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:08.660858Z'}]" +584,MVP,LockDam_05-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.85Z'}]" +585,MVP,LockDam_05-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:16.653Z'}]" +586,MVP,LockDam_05-TainterGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:42.298Z'}]" +587,MVP,LockDam_05-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:44.584Z'}]" +588,MVP,LockDam_05-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:43.502Z'}]" +589,MVP,LockDam_05-TainterGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:18.054Z'}]" +590,MVP,LockDam_05-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.963Z'}]" +591,MVP,LockDam_05-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:20.467Z'}]" +592,MVP,LockDam_05-TainterGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:57.924Z'}]" +593,MVP,LockDam_05-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:00.327Z'}]" +594,MVP,LockDam_05-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:02.92Z'}]" +595,MVP,LockDam_05-TainterGate14.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:25.251Z'}]" +596,MVP,LockDam_05-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:19.052Z'}]" +597,MVP,LockDam_05-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:22.877Z'}]" +598,MVP,LockDam_05-TainterGate15.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:17.14Z'}]" +599,MVP,LockDam_05-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:18.17Z'}]" +600,MVP,LockDam_05-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:18.089Z'}]" +601,MVP,LockDam_05-TainterGate16.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:35.525459Z'}]" +602,MVP,LockDam_05-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:25.459Z'}]" +603,MVP,LockDam_05-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:23.88Z'}]" +604,MVP,LockDam_05-TainterGate17.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:19.756Z'}]" +605,MVP,LockDam_05-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:20.751Z'}]" +606,MVP,LockDam_05-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:22.136Z'}]" +607,MVP,LockDam_05-TainterGate18.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:24.075Z'}]" +608,MVP,LockDam_05-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:08.951Z'}]" +609,MVP,LockDam_05-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:27.724Z'}]" +610,MVP,LockDam_05-TainterGate19.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:22.273Z'}]" +611,MVP,LockDam_05-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:23.514Z'}]" +612,MVP,LockDam_05-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:23.202Z'}]" +613,MVP,LockDam_05-TainterGate20.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:27.771102Z'}]" +614,MVP,LockDam_05-TainterGate20.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:29.379Z'}]" +615,MVP,LockDam_05-TainterGate20.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:29.354Z'}]" +616,MVP,LockDam_05-TainterGate21.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:33.556Z'}]" +617,MVP,LockDam_05-TainterGate21.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:34.776Z'}]" +618,MVP,LockDam_05-TainterGate21.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:34.639Z'}]" +619,MVP,LockDam_05-TainterGate22.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:59.413Z'}]" +620,MVP,LockDam_05-TainterGate22.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:30.294Z'}]" +621,MVP,LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:23.679Z'}]" +622,MVP,LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:23.798Z'}]" +623,MVP,LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:32:24.178Z'}]" +624,MVP,LockDam_05-TainterGate24.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.5Z'}]" +625,MVP,LockDam_05-TainterGate24.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.795Z'}]" +626,MVP,LockDam_05-TainterGate24.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:30.704Z'}]" +627,MVP,LockDam_05-TainterGate25.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:19.925Z'}]" +628,MVP,LockDam_05-TainterGate25.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.372Z'}]" +629,MVP,LockDam_05-TainterGate25.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:33:19.92Z'}]" +630,MVP,LockDam_05-TainterGate26.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.621Z'}]" +631,MVP,LockDam_05-TainterGate26.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:32.772Z'}]" +632,MVP,LockDam_05-TainterGate26.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:31.592Z'}]" +633,MVP,LockDam_05-TainterGate27.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:34.066Z'}]" +634,MVP,LockDam_05-TainterGate27.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:11:50.844Z'}]" +635,MVP,LockDam_05-TainterGate27.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:11:50.661Z'}]" +636,MVP,LockDam_05-TainterGate28.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:44.217699Z'}]" +637,MVP,LockDam_05-TainterGate28.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.969Z'}]" +638,MVP,LockDam_05-TainterGate28.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:38.261Z'}]" +639,MVP,LockDam_05-TainterGate29.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.954Z'}]" +640,MVP,LockDam_05-TainterGate29.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.982Z'}]" +641,MVP,LockDam_05-TainterGate29.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:16.886Z'}]" +642,MVP,LockDam_05-TainterGate30.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:58:08.617Z'}]" +643,MVP,LockDam_05-TainterGate30.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:22.111Z'}]" +644,MVP,LockDam_05-TainterGate31.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:48.66511Z'}]" +645,MVP,LockDam_05-TainterGate31.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:40.998Z'}]" +646,MVP,LockDam_05-TainterGate31.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:37.182Z'}]" +647,MVP,LockDam_05-TainterGate32.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:52.888841Z'}]" +648,MVP,LockDam_05-TainterGate32.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:40.16Z'}]" +649,MVP,LockDam_05-TainterGate32.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:38.565Z'}]" +650,MVP,LockDam_05-TainterGate33.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:57.754431Z'}]" +651,MVP,LockDam_05-TainterGate33.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:03.722585Z'}]" +652,MVP,LockDam_05-TainterGate33.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:13:47.713Z'}]" +653,MVP,LockDam_05-TainterGate34.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:16.532Z'}]" +654,MVP,LockDam_05-TainterGate34.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.963Z'}]" +655,MVP,LockDam_05-TainterGate34.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:16.558Z'}]" +656,MVP,LockDam_05-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:00.859541Z'}]" +657,MVP,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:03.572481Z'}]" +658,MVP,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:17.517279Z'}]" +659,MVP,LockDam_05-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:18.724126Z'}]" +660,MVP,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:51.939Z'}]" +661,MVP,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:08.281Z'}]" +662,MVP,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.125Z'}]" +663,MVP,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:51.824Z'}]" +664,MVP,LockDam_05-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:37.871Z'}]" +665,MVP,LockDam_05-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:39.164Z'}]" +666,MVP,LockDam_05-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:37.988Z'}]" +667,MVP,LockDam_05.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T13:00:00Z', 'latest-time': '2025-06-04T11:00:00Z', 'last-update': '2025-06-16T18:37:39.491Z'}]" +668,MVP,LockDam_05.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:20.233Z'}]" +669,MVP,LockDam_05.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:40.785Z'}]" +670,MVP,LockDam_05.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T13:00:00Z', 'latest-time': '2025-06-03T11:00:00Z', 'last-update': '2025-06-16T18:15:20.453Z'}]" +671,MVP,LockDam_05.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:15:25.296Z'}]" +672,MVP,LockDam_05.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-04T12:00:00Z', 'last-update': '2025-06-16T18:37:41.842Z'}]" +673,MVP,LockDam_05.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:15:26.758Z'}]" +674,MVP,LockDam_05.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:43.309Z'}]" +675,MVP,LockDam_05.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:58:45.348Z'}]" +676,MVP,LockDam_05.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.47Z'}]" +677,MVP,LockDam_05.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.708Z'}]" +678,MVP,LockDam_05.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.81Z'}]" +679,MVP,LockDam_05.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:31.940469Z'}]" +680,MVP,LockDam_05.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:30.586Z'}]" +681,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:48.314133Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:49.443519Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:45.928667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:48.087896Z'}]" +682,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:48.367Z'}]" +683,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:46.91Z'}]" +684,MVP,LockDam_05.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:51.001Z'}]" +685,MVP,LockDam_05.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:47.115Z'}]" +686,MVP,LockDam_05.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.317Z'}]" +687,MVP,LockDam_05.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.417Z'}]" +688,MVP,LockDam_05.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:50.631Z'}]" +689,MVP,LockDam_05.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:48.403Z'}]" +690,MVP,LockDam_05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.626Z'}]" +691,MVP,LockDam_05.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:53.321Z'}]" +692,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:54.4365Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:52.132615Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:53.165455Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:53.154478Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:37:50.651719Z'}]" +693,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:55.681Z'}]" +694,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:54.813Z'}]" +695,MVP,LockDam_05.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:55.925Z'}]" +696,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:56.106918Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:56.070753Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:56.999618Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:55.88586Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:37:56.106895Z'}]" +697,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:57.009Z'}]" +698,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:56.994Z'}]" +699,MVP,LockDam_05.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:57.02Z'}]" +700,MVP,LockDam_05.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.243Z'}]" +701,MVP,LockDam_05.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:57.257Z'}]" +702,MVP,LockDam_05.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.165Z'}]" +703,MVP,LockDam_05.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T15:00:00Z', 'latest-time': '2025-06-01T16:45:00Z', 'last-update': '2025-06-16T18:37:58.321Z'}]" +704,MVP,LockDam_05.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.535Z'}]" +705,MVP,LockDam_05.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.636Z'}]" +706,MVP,LockDam_05.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.734Z'}]" +707,MVP,LockDam_05.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.839Z'}]" +708,MVP,LockDam_05.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:59.479Z'}]" +709,MVP,LockDam_05.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:02.416Z'}]" +710,MVP,LockDam_05.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:02.007Z'}]" +711,MVP,LockDam_05.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T06:00:00Z', 'latest-time': '2025-06-12T05:00:00Z', 'last-update': '2025-06-16T18:38:02.018Z'}]" +712,MVP,LockDam_05.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:03.277Z'}]" +713,MVP,LockDam_05a-CrookedSlough.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:02.006Z'}]" +714,MVP,LockDam_05a-CrookedSlough.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:05.924Z'}]" +715,MVP,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:05.857Z'}]" +716,MVP,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:06.244Z'}]" +717,MVP,LockDam_05a-CrookedSlough.Temp-Water.Inst.1Hour.0.CEMVP-GOES-Raw,C,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:08.535Z'}]" +718,MVP,LockDam_05a-CrookedSlough.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:09.746Z'}]" +719,MVP,LockDam_05a-RollerGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.458Z'}]" +720,MVP,LockDam_05a-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:54.288Z'}]" +721,MVP,LockDam_05a-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:33:53.228Z'}]" +722,MVP,LockDam_05a-RollerGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.05Z'}]" +723,MVP,LockDam_05a-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.047Z'}]" +724,MVP,LockDam_05a-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:09.936Z'}]" +725,MVP,LockDam_05a-RollerGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.263Z'}]" +726,MVP,LockDam_05a-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.153Z'}]" +727,MVP,LockDam_05a-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:11.027Z'}]" +728,MVP,LockDam_05a-RollerGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:54.621557Z'}]" +729,MVP,LockDam_05a-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:47.397Z'}]" +730,MVP,LockDam_05a-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:36:47.049Z'}]" +731,MVP,LockDam_05a-RollerGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:18.779675Z'}]" +732,MVP,LockDam_05a-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:09.818Z'}]" +733,MVP,LockDam_05a-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:11.286Z'}]" +734,MVP,LockDam_05a-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.59Z'}]" +735,MVP,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.594Z'}]" +736,MVP,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.771Z'}]" +737,MVP,LockDam_05a-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.895Z'}]" +738,MVP,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:14.707Z'}]" +739,MVP,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.943Z'}]" +740,MVP,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:23.595Z'}]" +741,MVP,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:13.12Z'}]" +742,MVP,LockDam_05a-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:18.926Z'}]" +743,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.19Z'}]" +744,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.311Z'}]" +745,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:22.717Z'}]" +746,MVP,LockDam_05a-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:35.242Z'}]" +747,MVP,LockDam_05a-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:22.66Z'}]" +748,MVP,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-10T12:00:00Z', 'last-update': '2025-06-16T18:33:21.646Z'}]" +749,MVP,LockDam_05a-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:25.501Z'}]" +750,MVP,LockDam_05a-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-05-16T18:14:00Z', 'latest-time': '2025-06-11T22:15:00Z', 'last-update': '2025-06-16T18:33:26.492Z'}]" +751,MVP,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:26.583Z'}]" +752,MVP,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:31.394Z'}]" +753,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:27.577Z'}]" +754,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:31.606Z'}]" +755,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:37.71Z'}]" +756,MVP,LockDam_05a-TainterGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:22.626006Z'}]" +757,MVP,LockDam_05a-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:32:38.83779Z'}]" +758,MVP,LockDam_05a-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:59:13.209Z'}]" +759,MVP,LockDam_05a-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:14.609Z'}]" +760,MVP,LockDam_05a-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:59:14.374Z'}]" +761,MVP,LockDam_05a-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:32:33.58Z'}]" +762,MVP,LockDam_05a-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:32:31.046Z'}]" +763,MVP,LockDam_05a-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:34.15643Z'}]" +764,MVP,LockDam_05a-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:25.173Z'}]" +765,MVP,LockDam_05a-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:24.949Z'}]" +766,MVP,LockDam_05a-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:32.753012Z'}]" +767,MVP,LockDam_05a-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:15.671Z'}]" +768,MVP,LockDam_05a-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:16:13.528Z'}]" +769,MVP,LockDam_05a-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.333Z'}]" +770,MVP,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.6Z'}]" +771,MVP,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.673Z'}]" +772,MVP,LockDam_05a-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.558Z'}]" +773,MVP,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.616Z'}]" +774,MVP,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.485Z'}]" +775,MVP,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.77Z'}]" +776,MVP,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:35.381Z'}]" +777,MVP,LockDam_05a-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:54.826804Z'}]" +778,MVP,LockDam_05a-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:45.902Z'}]" +779,MVP,LockDam_05a-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:36:44.619Z'}]" +780,MVP,LockDam_05a.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T11:00:00Z', 'last-update': '2025-06-16T18:38:34.218Z'}]" +781,MVP,LockDam_05a.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:29.956107Z'}]" +782,MVP,LockDam_05a.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:36.278Z'}]" +783,MVP,LockDam_05a.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T11:00:00Z', 'last-update': '2025-06-16T18:38:35.195Z'}]" +784,MVP,LockDam_05a.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:42.765512Z'}]" +785,MVP,LockDam_05a.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:41.443Z'}]" +786,MVP,LockDam_05a.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:59:24.776Z'}]" +787,MVP,LockDam_05a.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:42.435Z'}]" +788,MVP,LockDam_05a.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:37.624699Z'}]" +789,MVP,LockDam_05a.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:39.210636Z'}]" +790,MVP,LockDam_05a.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:39.04818Z'}]" +791,MVP,LockDam_05a.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:35.129195Z'}]" +792,MVP,LockDam_05a.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:31.194Z'}]" +793,MVP,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:39.173Z'}]" +794,MVP,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:39.272Z'}]" +795,MVP,LockDam_05a.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:38:39.222Z'}]" +796,MVP,LockDam_05a.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.171Z'}]" +797,MVP,LockDam_05a.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.446Z'}]" +798,MVP,LockDam_05a.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.119Z'}]" +799,MVP,LockDam_05a.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.394Z'}]" +800,MVP,LockDam_05a.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:40.118Z'}]" +801,MVP,LockDam_05a.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.496Z'}]" +802,MVP,LockDam_05a.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:41.4Z'}]" +803,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:38:41.605968Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:38:41.691065Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:38:41.527687Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:38:42.755259Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:38:41.379166Z'}]" +804,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:42.784Z'}]" +805,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:42.782Z'}]" +806,MVP,LockDam_05a.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:44.092Z'}]" +807,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:38:45.25246Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:38:44.3434Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:38:44.285624Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:38:46.678264Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:38:44.02367Z'}]" +808,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:45.492Z'}]" +809,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:45.387Z'}]" +810,MVP,LockDam_05a.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:45.387Z'}]" +811,MVP,LockDam_05a.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.737Z'}]" +812,MVP,LockDam_05a.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:46.678Z'}]" +813,MVP,LockDam_05a.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:46.616Z'}]" +814,MVP,LockDam_05a.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-05-16T18:06:00Z', 'latest-time': '2025-05-16T18:06:00Z', 'last-update': '2025-06-16T18:38:47.965Z'}]" +815,MVP,LockDam_05a.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.799Z'}]" +816,MVP,LockDam_05a.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.899Z'}]" +817,MVP,LockDam_05a.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:49.134Z'}]" +818,MVP,LockDam_05a.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:48Z'}]" +819,MVP,LockDam_05a.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:47.722Z'}]" +820,MVP,LockDam_05a.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:48.202Z'}]" +821,MVP,LockDam_05a.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:51.986Z'}]" +822,MVP,LockDam_05a.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:49.399Z'}]" +823,MVP,MissHW_Gull-Fishway.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:35.052Z'}]" +824,MVP,MissHW_Gull-Fishway.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:43:33.948Z'}]" +825,MVP,MissHW_Gull-Lake.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:03.159Z'}]" +826,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:00.621Z'}]" +827,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:04.235Z'}]" +828,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:04.431Z'}]" +829,MVP,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:05.404Z'}]" +830,MVP,MissHW_Gull-Lake.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:07.137Z'}]" +831,MVP,MissHW_Gull-Lake.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:43:00Z', 'latest-time': '2025-04-21T17:06:00Z', 'last-update': '2025-06-16T18:35:05.827Z'}]" +832,MVP,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:07.127Z'}]" +833,MVP,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:09.34Z'}]" +834,MVP,MissHW_Gull-Lake.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:06.723Z'}]" +835,MVP,MissHW_Gull-Lake.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:10.521Z'}]" +836,MVP,MissHW_Gull-Lake.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:11.921Z'}]" +837,MVP,MissHW_Gull-SlideGate01.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.641Z'}]" +838,MVP,MissHW_Gull-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.979Z'}]" +839,MVP,MissHW_Gull-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:33:18.718Z'}]" +840,MVP,MissHW_Gull-SlideGate02.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:27.844Z'}]" +841,MVP,MissHW_Gull-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:28.864Z'}]" +842,MVP,MissHW_Gull-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:27.859Z'}]" +843,MVP,MissHW_Gull-SlideGate03.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:18.451Z'}]" +844,MVP,MissHW_Gull-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:20.95Z'}]" +845,MVP,MissHW_Gull-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:35:18.125Z'}]" +846,MVP,MissHW_Gull-SlideGate04.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:23.85Z'}]" +847,MVP,MissHW_Gull-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:26.648Z'}]" +848,MVP,MissHW_Gull-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:30.232Z'}]" +849,MVP,MissHW_Gull-SlideGate05.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:28.723Z'}]" +850,MVP,MissHW_Gull-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:29.646Z'}]" +851,MVP,MissHW_Gull-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:35:37.143Z'}]" +852,MVP,MissHW_Gull-StopLog01.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:29.044Z'}]" +853,MVP,MissHW_Gull-StopLog01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:29.178Z'}]" +854,MVP,MissHW_Gull-StopLog01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:28.954Z'}]" +855,MVP,MissHW_Gull-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:13:23.351Z'}]" +856,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.merged-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:11.943Z'}]" +857,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NAVD88,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:12.132Z'}]" +858,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:13.22Z'}]" +859,MVP,MissHW_Gull-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:13.227Z'}]" +860,MVP,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:15.73Z'}]" +861,MVP,MissHW_Gull-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:14.441Z'}]" +862,MVP,MissHW_Gull-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T18:18:00Z', 'latest-time': '2025-04-21T19:53:00Z', 'last-update': '2025-06-16T18:13:27.179Z'}]" +863,MVP,MissHW_Gull-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-22T12:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:15.957Z'}]" +864,MVP,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.926Z'}]" +865,MVP,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.82Z'}]" +866,MVP,MissHW_Gull-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.819Z'}]" +867,MVP,MissHW_Gull.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-01T13:00:00Z', 'latest-time': '2025-05-23T13:00:00Z', 'last-update': '2025-06-16T18:43:37.56Z'}]" +868,MVP,MissHW_Gull.Area.Inst.1Hour.0.comp,m2,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:38.95Z'}]" +869,MVP,MissHW_Gull.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-01T13:00:00Z', 'latest-time': '2025-04-01T13:00:00Z', 'last-update': '2025-06-16T18:43:38.744Z'}]" +870,MVP,MissHW_Gull.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:03:03.327Z'}]" +871,MVP,MissHW_Gull.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-10T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:38.917Z'}]" +872,MVP,MissHW_Gull.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:39.042Z'}]" +873,MVP,MissHW_Gull.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,deg,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:39.108Z'}]" +874,MVP,MissHW_Gull.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:03:04.642Z'}]" +875,MVP,MissHW_Gull.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:39.952Z'}]" +876,MVP,MissHW_Gull.Elev.Inst.1Hour.0.merged-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:40.165Z'}]" +877,MVP,MissHW_Gull.Elev.Inst.1Hour.0.rev-NAVD88,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:40.254Z'}]" +878,MVP,MissHW_Gull.Elev.Inst.1Hour.0.rev-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:42.674Z'}]" +879,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:44.00339Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:45.040629Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:43.965972Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:46.319607Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:43.780927Z'}]" +880,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.062Z'}]" +881,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:43.947Z'}]" +882,MVP,MissHW_Gull.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.085Z'}]" +883,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:45.134Z'}]" +884,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:43:45.258Z'}]" +885,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:43:45.245Z'}]" +886,MVP,MissHW_Gull.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:45.265Z'}]" +887,MVP,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.295Z'}]" +888,MVP,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.411Z'}]" +889,MVP,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.425Z'}]" +890,MVP,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.709609Z'}]" +891,MVP,MissHW_Gull.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.503Z'}]" +892,MVP,MissHW_Gull.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.563Z'}]" +893,MVP,MissHW_Gull.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.718Z'}]" +894,MVP,MissHW_Gull.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:47.653Z'}]" +895,MVP,MissHW_Gull.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.667Z'}]" +896,MVP,MissHW_Gull.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.721Z'}]" +897,MVP,MissHW_Gull.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.767Z'}]" +898,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:48.983964Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:48.989779Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:49.153614Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:47.953304Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:50.196813Z'}]" +899,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:48.926Z'}]" +900,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:49.106Z'}]" +901,MVP,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:43:48.977Z'}]" +902,MVP,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:49.209Z'}]" +903,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:50.370073Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:51.455235Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:52.623374Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:52.776798Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:50.207866Z'}]" +904,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:50.247Z'}]" +905,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:50.342Z'}]" +906,MVP,MissHW_Gull.Flow.Inst.1Hour.0.comp-gates,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:50.39Z'}]" +907,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:51.870819Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:51.716895Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:51.585669Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:53.934913Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:51.441324Z'}]" +908,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:51.56Z'}]" +909,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:51.635Z'}]" +910,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:51.888956Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:52.646769Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:53.971513Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:51.865465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:51.731686Z'}]" +911,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:51.87Z'}]" +912,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:52.642Z'}]" +913,MVP,MissHW_Gull.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:51.93Z'}]" +914,MVP,MissHW_Gull.Precip-cum.Inst.1Hour.0.rev,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:52.662Z'}]" +915,MVP,MissHW_Gull.Precip-inc.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-05-01T14:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:52.797Z'}]" +916,MVP,MissHW_Gull.Precip-inc.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-04-21T17:00:00Z', 'latest-time': '2025-05-01T18:00:00Z', 'last-update': '2025-06-16T18:43:52.706Z'}]" +917,MVP,MissHW_Gull.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-23T00:00:00Z', 'last-update': '2025-06-16T18:43:52.788Z'}]" +918,MVP,MissHW_Gull.Precip-inc.Total.1Hour.1Hour.comp,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:54.202967Z'}]" +919,MVP,MissHW_Gull.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:53.973266Z'}]" +920,MVP,MissHW_Gull.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,kph,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:52.945Z'}]" +921,MVP,MissHW_Gull.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:52.959Z'}]" +922,MVP,MissHW_Gull.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:53.904Z'}]" +923,MVP,MissHW_Gull.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T17:57:00Z', 'latest-time': '2025-04-21T17:28:00Z', 'last-update': '2025-06-16T18:43:54.05Z'}]" +924,MVP,MissHW_Gull.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-22T12:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:58.014123Z'}]" +925,MVP,MissHW_Gull.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.184Z'}]" +926,MVP,MissHW_Gull.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.098Z'}]" +927,MVP,MissHW_Gull.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.144Z'}]" +928,MVP,MissHW_Gull.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:54.203Z'}]" +929,MVP,MissHW_Gull.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:55.174Z'}]" +930,MVP,MissHW_Gull.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.218Z'}]" +931,MVP,MissHW_Gull.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.254Z'}]" +932,MVP,MissHW_Gull.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.283Z'}]" +933,MVP,MissHW_Gull.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:57.90317Z'}]" +934,MVP,MissHW_PineRiver-SlideGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:02.144Z'}]" +935,MVP,MissHW_PineRiver-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:18.81Z'}]" +936,MVP,MissHW_PineRiver-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:04.577Z'}]" +937,MVP,MissHW_PineRiver-SlideGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.526Z'}]" +938,MVP,MissHW_PineRiver-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:13.695Z'}]" +939,MVP,MissHW_PineRiver-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:17.1Z'}]" +940,MVP,MissHW_PineRiver-SlideGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:19.859Z'}]" +941,MVP,MissHW_PineRiver-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:33.05Z'}]" +942,MVP,MissHW_PineRiver-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:19.894Z'}]" +943,MVP,MissHW_PineRiver-SlideGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:41.917Z'}]" +944,MVP,MissHW_PineRiver-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:41.822Z'}]" +945,MVP,MissHW_PineRiver-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:33:41.58Z'}]" +946,MVP,MissHW_PineRiver-SlideGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:34.495Z'}]" +947,MVP,MissHW_PineRiver-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:35.7Z'}]" +948,MVP,MissHW_PineRiver-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:21.335Z'}]" +949,MVP,MissHW_PineRiver-SlideGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:04.855Z'}]" +950,MVP,MissHW_PineRiver-SlideGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:23.511Z'}]" +951,MVP,MissHW_PineRiver-SlideGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:04.782Z'}]" +952,MVP,MissHW_PineRiver-SlideGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:05.945Z'}]" +953,MVP,MissHW_PineRiver-SlideGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:06.138Z'}]" +954,MVP,MissHW_PineRiver-SlideGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:05.862Z'}]" +955,MVP,MissHW_PineRiver-SlideGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.058Z'}]" +956,MVP,MissHW_PineRiver-SlideGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:27.216Z'}]" +957,MVP,MissHW_PineRiver-SlideGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:06.028Z'}]" +958,MVP,MissHW_PineRiver-SlideGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.045Z'}]" +959,MVP,MissHW_PineRiver-SlideGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.345Z'}]" +960,MVP,MissHW_PineRiver-SlideGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:07.213Z'}]" +961,MVP,MissHW_PineRiver-SlideGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.441Z'}]" +962,MVP,MissHW_PineRiver-SlideGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:08.645Z'}]" +963,MVP,MissHW_PineRiver-SlideGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:08.25Z'}]" +964,MVP,MissHW_PineRiver-SlideGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:16.165Z'}]" +965,MVP,MissHW_PineRiver-SlideGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:13.563Z'}]" +966,MVP,MissHW_PineRiver-SlideGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:11.06Z'}]" +967,MVP,MissHW_PineRiver-SlideGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:19.682Z'}]" +968,MVP,MissHW_PineRiver-SlideGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:18.638Z'}]" +969,MVP,MissHW_PineRiver-SlideGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:17.581Z'}]" +970,MVP,MissHW_PineRiver-SlideGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:19.759Z'}]" +971,MVP,MissHW_PineRiver-SlideGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:37.718Z'}]" +972,MVP,MissHW_PineRiver-SlideGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:20.993Z'}]" +973,MVP,MissHW_PineRiver-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:21.091Z'}]" +974,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.409Z'}]" +975,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.478Z'}]" +976,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:21.37Z'}]" +977,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.18Z'}]" +978,MVP,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:21.268Z'}]" +979,MVP,MissHW_PineRiver-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.257Z'}]" +980,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-04-21T22:52:00Z', 'latest-time': '2025-04-21T22:52:00Z', 'last-update': '2025-06-16T18:34:23.467Z'}]" +981,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:24.743Z'}]" +982,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:26.173Z'}]" +983,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:41.532Z'}]" +984,MVP,MissHW_PineRiver.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T13:00:00Z', 'latest-time': '2025-04-07T13:00:00Z', 'last-update': '2025-06-16T18:12:39.91Z'}]" +985,MVP,MissHW_PineRiver.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:45.053Z'}]" +986,MVP,MissHW_PineRiver.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:31.558Z'}]" +987,MVP,MissHW_PineRiver.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:12:40.342Z'}]" +988,MVP,MissHW_PineRiver.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:43.944Z'}]" +989,MVP,MissHW_PineRiver.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,deg,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:47.848044Z'}]" +990,MVP,MissHW_PineRiver.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:12:42.697Z'}]" +991,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:49.247Z'}]" +992,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:48.938Z'}]" +993,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:33.61Z'}]" +994,MVP,MissHW_PineRiver.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:31.576Z'}]" +995,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:35.099117Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:34.912272Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:32.794075Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:32.602972Z'}]" +996,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:32.651Z'}]" +997,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:34.982Z'}]" +998,MVP,MissHW_PineRiver.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:34.982Z'}]" +999,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:33.79Z'}]" +1000,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:34:33.866Z'}]" +1001,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:34:35.068Z'}]" +1002,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:34:36.151Z'}]" +1003,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:33.921Z'}]" +1004,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:35.31Z'}]" +1005,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:36.386Z'}]" +1006,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:35.166Z'}]" +1007,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:36.33Z'}]" +1008,MVP,MissHW_PineRiver.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.447Z'}]" +1009,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:38.811852Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:37.588394Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:40.092218Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:37.78494Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:36.554876Z'}]" +1010,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.785Z'}]" +1011,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:37.443Z'}]" +1012,MVP,MissHW_PineRiver.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.592Z'}]" +1013,MVP,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:40.257Z'}]" +1014,MVP,MissHW_PineRiver.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:39.977Z'}]" +1015,MVP,MissHW_PineRiver.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:38.951Z'}]" +1016,MVP,MissHW_PineRiver.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.675Z'}]" +1017,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:40.387616Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:41.403035Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:40.022999Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:38.731655Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:38.992884Z'}]" +1018,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:41.267Z'}]" +1019,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:40.301Z'}]" +1020,MVP,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:41.437Z'}]" +1021,MVP,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:42.732Z'}]" +1022,MVP,MissHW_PineRiver.Flow.Inst.15Minutes.0.comp-gates,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:42.672Z'}]" +1023,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:45.204703Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:45.394003Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:44.038404Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:45.137575Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:43.830993Z'}]" +1024,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:45.271Z'}]" +1025,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:45.191Z'}]" +1026,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:46.459963Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:44.091152Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:44.20912Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:45.118593Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:43.856872Z'}]" +1027,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:45.302Z'}]" +1028,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:45.18Z'}]" +1029,MVP,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.398Z'}]" +1030,MVP,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:47.908Z'}]" +1031,MVP,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:47.587Z'}]" +1032,MVP,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.rev,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.478Z'}]" +1033,MVP,MissHW_PineRiver.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.702Z'}]" +1034,MVP,MissHW_PineRiver.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.381Z'}]" +1035,MVP,MissHW_PineRiver.Precip-inc.Total.1Hour.1Hour.comp,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.687Z'}]" +1036,MVP,MissHW_PineRiver.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:50.287Z'}]" +1037,MVP,MissHW_PineRiver.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:50.317Z'}]" +1038,MVP,MissHW_PineRiver.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,kph,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:51.422Z'}]" +1039,MVP,MissHW_PineRiver.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:48.862Z'}]" +1040,MVP,MissHW_PineRiver.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:51.777Z'}]" +1041,MVP,MissHW_PineRiver.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-04-21T23:00:00Z', 'latest-time': '2025-04-21T23:00:00Z', 'last-update': '2025-06-16T18:34:51.802Z'}]" +1042,MVP,MissHW_PineRiver.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:53.116Z'}]" +1043,MVP,MissHW_PineRiver.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:53.976Z'}]" +1044,MVP,MissHW_PineRiver.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:54.346Z'}]" +1045,MVP,MissHW_PineRiver.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:52.803Z'}]" +1046,MVP,MissHW_PineRiver.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:54.099Z'}]" +1047,MVP,MissHW_PineRiver.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:54.111Z'}]" +1048,MVP,MissHW_PineRiver.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:55.434Z'}]" +1049,MVP,MissHW_PineRiver.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:55.528Z'}]" +1050,MVP,MissHW_PineRiver.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:57.01Z'}]" +1051,MVP,Muscoda.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:07.425Z'}]" +1052,MVP,Muscoda.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:04.714Z'}]" +1053,MVP,Muscoda.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:06.422Z'}]" +1054,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:08.700079Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:07.583556Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:07.426816Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:06.421326Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:05.165804Z'}]" +1055,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:08.52Z'}]" +1056,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:08.887Z'}]" +1057,MVP,Muscoda.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-04T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T18:42:08.967Z'}]" +1058,MVP,Muscoda.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:10.042Z'}]" +1059,MVP,Muscoda.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:10.137Z'}]" +1060,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:11.452838Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:13.738115Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:13.597948Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:12.329473Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:11.200671Z'}]" +1061,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:12.285Z'}]" +1062,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:12.419Z'}]" +1063,MVP,Muscoda.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-02T21:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:12.529Z'}]" +1064,MVP,Muscoda.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-02T21:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:13.84Z'}]" +1065,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:14.843459Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:14.853578Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:13.93776Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:13.933483Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:13.694114Z'}]" +1066,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:15.019Z'}]" +1067,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:14.813Z'}]" +1068,MVP,Muscoda.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.091Z'}]" +1069,MVP,Muscoda.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.235Z'}]" +1070,MVP,Muscoda.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.09Z'}]" +1071,MVP,Muscoda.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:15.146Z'}]" +1072,MVP,Muscoda.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.542Z'}]" +1073,MVP,Muscoda.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.438Z'}]" +1074,MVP,Muscoda.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:18.798Z'}]" +1075,MVP,Muscoda.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:21.339Z'}]" +1076,MVP,Muscoda.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-22T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:20.025Z'}]" +1077,MVP,Muscoda.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:22.555Z'}]" +1078,MVP,Muscoda.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:21.567Z'}]" +1079,MVP,NWUM5.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:30.081Z'}]" +1080,MVP,NWUM5.Elev.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:30.563Z'}]" +1081,MVP,NWUM5.Elev.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:32.684Z'}]" +1082,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:33.911783Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:32.88008Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:31.798013Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:33.942497Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:31.639567Z'}]" +1083,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:33.944Z'}]" +1084,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:34.165Z'}]" +1085,MVP,NWUM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-13T05:00:00Z', 'latest-time': '2025-05-10T05:00:00Z', 'last-update': '2025-06-16T18:42:34.345Z'}]" +1086,MVP,NWUM5.Flow.Inst.0.0.Raw-USGS,cms,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-12T18:22:00Z', 'latest-time': '2025-05-06T17:29:00Z', 'last-update': '2025-06-16T18:42:34.263Z'}]" +1087,MVP,NWUM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:35.472Z'}]" +1088,MVP,NWUM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:35.564Z'}]" +1089,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:38.075857Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:37.76879Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:37.694397Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:38.982635Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:36.703753Z'}]" +1090,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:39.002Z'}]" +1091,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:38.941Z'}]" +1092,MVP,NWUM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T22:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:40.232Z'}]" +1093,MVP,NWUM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T22:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:39.369Z'}]" +1094,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:41.778234Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:40.613757Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:40.588447Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:40.567401Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:40.372884Z'}]" +1095,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:41.655Z'}]" +1096,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:41.613Z'}]" +1097,MVP,NWUM5.Stage.Inst.0.0.Raw-USGS,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-12T18:22:00Z', 'latest-time': '2025-05-06T17:29:00Z', 'last-update': '2025-06-16T18:42:41.629Z'}]" +1098,MVP,NWUM5.Stage.Inst.15Minutes.0.OTHER-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.827Z'}]" +1099,MVP,NWUM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.881Z'}]" +1100,MVP,NWUM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:44.166Z'}]" +1101,MVP,NWUM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.976Z'}]" +1102,MVP,NWUM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-11T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.866Z'}]" +1103,MVP,NWUM5.Volt.Inst.1Hour.0.OTHER-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-20T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:43.193Z'}]" +1104,MVP,Rafferty_Dam-Tailwater.Flow.Inst.5Minutes.0.Raw-EnvCan,cms,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:19.520532Z'}]" +1105,MVP,Rafferty_Dam-Tailwater.Stage.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:08.058Z'}]" +1106,MVP,Rafferty_Dam.Elev.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.601075Z'}]" +1107,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:59.011086Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:59.121663Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:56.829077Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:57.749501Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:56.687811Z'}]" +1108,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:56.593Z'}]" +1109,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:56.835Z'}]" +1110,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.578845Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:00.436975Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:00.696008Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:00.304716Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:57.909433Z'}]" +1111,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:58.03Z'}]" +1112,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:58.028Z'}]" +1113,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:59.211311Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:01.565275Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:59.23332Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:00.29726Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:59.10088Z'}]" +1114,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:59.361Z'}]" +1115,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:59.234Z'}]" +1116,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.340506Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:59.69923Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:59.673483Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:59.560864Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:01.568751Z'}]" +1117,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:00.294Z'}]" +1118,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:44:00.438Z'}]" +1119,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.709286Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:01.563956Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:03.024022Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:02.905741Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:00.596076Z'}]" +1120,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:00.695Z'}]" +1121,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:44:00.712Z'}]" +1122,MVP,Rafferty_Dam.Stage.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:29.820115Z'}]" +1123,MVP,TraverseWR_Dam-Lake.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:20.879Z'}]" +1124,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.933Z'}]" +1125,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.042Z'}]" +1126,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.066Z'}]" +1127,MVP,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:21.935Z'}]" +1128,MVP,TraverseWR_Dam-Lake.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:22.124Z'}]" +1129,MVP,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-13T02:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.326Z'}]" +1130,MVP,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:24.352Z'}]" +1131,MVP,TraverseWR_Dam-Lake.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:25.679Z'}]" +1132,MVP,TraverseWR_Dam-Lake.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.487Z'}]" +1133,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-15T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:24.604Z'}]" +1134,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:24.653Z'}]" +1135,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.504Z'}]" +1136,MVP,TraverseWR_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:42.41Z'}]" +1137,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:44.675Z'}]" +1138,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:44.991Z'}]" +1139,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.38Z'}]" +1140,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:46.282Z'}]" +1141,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.385Z'}]" +1142,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.646Z'}]" +1143,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-31T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:46.182Z'}]" +1144,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-31T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:48.801Z'}]" +1145,MVP,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:47.299Z'}]" +1146,MVP,TraverseWR_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:50.102Z'}]" +1147,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:51.441Z'}]" +1148,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.877Z'}]" +1149,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.729Z'}]" +1150,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:56.261Z'}]" +1151,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:56.348Z'}]" +1152,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.877Z'}]" +1153,MVP,TraverseWR_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:43.332Z'}]" +1154,MVP,TraverseWR_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:46.72Z'}]" +1155,MVP,TraverseWR_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:33:43.144Z'}]" +1156,MVP,TraverseWR_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:25.948Z'}]" +1157,MVP,TraverseWR_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:28.249Z'}]" +1158,MVP,TraverseWR_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:40:27.213Z'}]" +1159,MVP,TraverseWR_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:06.104124Z'}]" +1160,MVP,TraverseWR_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:58.45Z'}]" +1161,MVP,TraverseWR_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:12:17.431Z'}]" +1162,MVP,TraverseWR_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:40.781Z'}]" +1163,MVP,TraverseWR_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:52.491Z'}]" +1164,MVP,TraverseWR_Dam.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-22T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:53.586Z'}]" +1165,MVP,TraverseWR_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:53.782Z'}]" +1166,MVP,TraverseWR_Dam.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:55.217Z'}]" +1167,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.491Z'}]" +1168,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.193Z'}]" +1169,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.302Z'}]" +1170,MVP,TraverseWR_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:41.1Z'}]" +1171,MVP,TraverseWR_Dam.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:01.504Z'}]" +1172,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:06.555468Z'}]" +1173,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:59.893Z'}]" +1174,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:57.316Z'}]" +1175,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:05.313633Z'}]" +1176,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:56.413Z'}]" +1177,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:41:01.403Z'}]" +1178,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-04T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:41:00.246Z'}]" +1179,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:58.988Z'}]" +1180,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:59.004Z'}]" +1181,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:02.823739Z'}]" +1182,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:03.998Z'}]" +1183,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:02.898Z'}]" +1184,MVP,TraverseWR_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:06.493Z'}]" +1185,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:07.475901Z'}]" +1186,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:01:06.463Z'}]" +1187,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:18:52.473Z'}]" +1188,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:10.272837Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:10.073007Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:08.909855Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:11.336012Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:11.408089Z'}]" +1189,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.01Z'}]" +1190,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:10.12Z'}]" +1191,MVP,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:10.216Z'}]" +1192,MVP,TraverseWR_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:11.509Z'}]" +1193,MVP,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.323Z'}]" +1194,MVP,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.395Z'}]" +1195,MVP,TraverseWR_Dam.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.4Z'}]" +1196,MVP,TraverseWR_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:11.298Z'}]" +1197,MVP,TraverseWR_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.44Z'}]" +1198,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:12.719742Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:13.846029Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:15.170814Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:13.995Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:12.849292Z'}]" +1199,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:12.57Z'}]" +1200,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:12.913Z'}]" +1201,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:15.52092Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:17.897955Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:18.974952Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:17.706871Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:15.320425Z'}]" +1202,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:15.257Z'}]" +1203,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:15.206Z'}]" +1204,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:17.700779Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:16.61681Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:17.947231Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:16.440211Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:18.969499Z'}]" +1205,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.896Z'}]" +1206,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:18.938Z'}]" +1207,MVP,TraverseWR_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-11T13:00:00Z', 'last-update': '2025-06-16T18:41:16.454Z'}]" +1208,MVP,TraverseWR_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:16.619Z'}]" +1209,MVP,TraverseWR_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:16.747Z'}]" +1210,MVP,TraverseWR_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.677Z'}]" +1211,MVP,TraverseWR_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.939Z'}]" +1212,MVP,TraverseWR_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:19.226Z'}]" +1213,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:19.122Z'}]" +1214,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.544Z'}]" +1215,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.346Z'}]" +1216,MVP,TraverseWR_Dam.Stor.Ave.15Minutes.2Hours.comp,m3,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.436Z'}]" +1217,MVP,TraverseWR_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:19.194Z'}]" +1218,MVP,TraverseWR_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.18Z'}]" +1219,MVP,TraverseWR_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:20.673Z'}]" +1220,MVP,TraverseWR_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:21.629Z'}]" +1221,MVP,TraverseWR_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:22.836Z'}]" +1222,MVP,TraverseWR_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:25.332471Z'}]" +1223,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:34.180276Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:33.007808Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:32.704893Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:34.24004Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:32.991601Z'}]" +1224,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:41.138Z'}]" +1225,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:34.262Z'}]" +1226,MVP,ZUMM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:36.509Z'}]" +1227,MVP,ZUMM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:42.734Z'}]" +1228,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:37.962076Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:36.733914Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:39.041481Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:37.823873Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:36.50295Z'}]" +1229,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:36.888Z'}]" +1230,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:36.807Z'}]" +1231,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:39.257429Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:39.100908Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:40.366749Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:40.609429Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:38.101221Z'}]" +1232,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.203Z'}]" +1233,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:39.133Z'}]" +1234,MVP,ZUMM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.409Z'}]" +1235,MVP,ZUMM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.459Z'}]" +1236,MVP,ZUMM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.41Z'}]" +1237,MVP,ZUMM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.53Z'}]" +1238,MVP,ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.668Z'}]" +1239,MVP,ZUMM5.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:42.799Z'}]" diff --git a/load_data/base_locations_to_grab.csv b/load_data/base_locations_to_grab.csv new file mode 100644 index 0000000000..475398f07b --- /dev/null +++ b/load_data/base_locations_to_grab.csv @@ -0,0 +1,36 @@ +Office,Base_Location +MVP,ABRN8 +MVP,AGYM5 +MVP,Baldhill_Dam +MVP,ChippewaDiv_Dam +MVP,Clayton +MVP,Cooperstown +MVP,DAWM5 +MVP,ELZM5 +MVP,Highway75_Dam +MVP,LFKM5 +MVP,LockDam_05 +MVP,LockDam_02 +MVP,MissHW_Gull +MVP,MOOM5S +MVP,MissHW_PineRiver +MVP,Muscoda +MVP,NWUM5 +MVP,Rafferty_Dam +MVP,TraverseWR_Dam +MVP,ZUMM5 +LRL,ALVK2 +LRL,BKVI3 +LRL,DUNK2 +LRL,GRLK2 +LRL,Barkley +LRL,Cairo +LRL,Kentucky +LRL,Newburgh +LRL,Olmsted +LRL,Boston +LRL,Catawba +LRL,Cincinnati +LRL,Buckhorn +LRL,Green +LRL,Taylorsville diff --git a/load_data/data/.ipynb_checkpoints/LRL_locations_data-checkpoint.csv b/load_data/data/.ipynb_checkpoints/LRL_locations_data-checkpoint.csv new file mode 100755 index 0000000000..b8219d8e3f --- /dev/null +++ b/load_data/data/.ipynb_checkpoints/LRL_locations_data-checkpoint.csv @@ -0,0 +1,53 @@ +office,name,kind,unit,nation,active,aliases,nearest-city,time-zone,latitude,longitude,horizontal-datum,elevation,vertical-datum,state,county,bounding-office,map-label,public-name,long-name,type,published-latitude,published-longitude,description +LRL,Taylorsville-Lake,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295597'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Lake'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Lake'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,466.84999999999997,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Green-GRLK2,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03007-GRLK2'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,203.6064,NGVD29,KY,Taylor,LRL,Green River Lake,,,,,, +LRL,Boston-BSNK2L,SITE,m,United States,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE693326-BSNK2L'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03301500-BSNK2L'}]",,US/Eastern,37.767286111111,-85.70385,,122.048016,NGVD29,KY,Nelson,,,,,,,, +LRL,Green,PROJECT,ft,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03007'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,668.0,NGVD29,KY,Taylor,LRL,Green River Lake,Green River Lake,Green,Corps Reservoir,,, +LRL,Taylorsville-Bypass02,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Bypass02'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Bypass02'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Catawba,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03253500'}]","Falmouth, KY",US/Eastern,38.7103478,-84.3107693,NAD27,500.01,NGVD29,KY,Pendleton,LRL,Licking River at Catawba,"LICKING RIVER AT CATAWBA, KY","LICKING RIVER AT CATAWBA, KY",Stream Gauge,,, +LRL,Buckhorn-Lake,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03280800'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY03027-Lake'}]","Buckhorn, KY",US/Eastern,37.3402778,-83.4691667,NAD27,757.0,NGVD29,KY,Perry,LRL,Buckhorn Lake,,,,,, +LRL,TaylorsvilleOH,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03263000'}]",,America/New_York,,,,,,,,,,,,,,, +LRL,DUNK2S,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,BKVI3,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Cincinnati-CCNO1,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03255000-CCNO1'}]","Covington, KY",US/Eastern,39.0945044,-84.5104983,NAD27,428.87999999999994,NGVD29,OH,Hamilton,LRL,Ohio River at Cincinnati,,,,,, +LRL,Olmsted-HW,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-HW'}]",Presque Isle,Unknown or Not Applicable,0.0,0.0,,0.0,,00,Unknown County or County N/A for Unknown State or State N/A,,,Olmsted Headwater,,,-3.4028234663852886e+38,-3.4028234663852886e+38, +LRL,Boston,SITE,ft,,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE693326'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03301500'}]",,US/Eastern,37.767286111111,-85.70385,,400.41999999999996,NGVD29,KY,Nelson,,,"ROLLING FORK NEAR BOSTON, KY","ROLLING FORK NEAR BOSTON, KY",,,, +LRL,ALVK2,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-Outlet01,OUTLET,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Outlet01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Outlet01'}]","Taylorsville, KY",US/Eastern,38.005360231786,-85.307965492868,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,Taylorsville Outlet,,,,, +LRL,Taylorsville-BRCK2,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-BRCK2'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-BRCK2'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Green-Lake,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03305990'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY03007-Lake'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,203.6064,NGVD29,KY,Taylor,LRL,Green River Lake,,,,,, +LRL,Greensburg,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03306500'}]","Greensburg, KY",US/Central,37.2536713,-85.5030215,NAD83,531.81,NGVD29,KY,Green,LRL,,"GREEN RIVER AT GREENSBURG, KY","GREEN RIVER AT GREENSBURG, KY",,,, +LRL,Cairo,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '370000089094501'}]",,,,,,,,,,,,,,,,, +LRL,BowlingGreenKY-BWGK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE774640-BWGK2L'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03314500-BWGK2L'}]",,US/Central,37.002819444444,-86.432766666667,,409.83,NGVD29,KY,Warren,,,,,,,, +LRL,Franklin-ALVK2S,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03313700-ALVK2S'}]","Franklin, KY",US/Central,36.7233738,-86.5522175,NAD83,174.799752,NGVD29,KY,Simpson,LRL,,,,,,, +LRL,Newburgh,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03304300'}]","Newburgh, IN",US/Central,37.934301333332,-87.379384665112,NAD83,100.434648,NGVD29,IN,Warrick,LRL,Newburgh Locks and Dam TW,OHIO RIVER AT NEWBURGH LOCK AND ,"OHIO RIVER AT NEWBURGH LOCK AND DAM, IN",Stream Gauge,,, +LRL,Alvaton-ALVK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03314000-ALVK2L'}, {'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE6D10F0-ALVK2L'}]",,US/Central,36.895319444444,-86.380547222222,,443.0699999999999,NGVD29,KY,Warren,,,,,,,, +LRL,Taylorsville-Tailwater,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Tailwater'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Barkley-BARK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,GRLK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Buckhorn,PROJECT,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03027'}]","Buckhorn, KY",US/Eastern,37.3402778,-83.4691667,NAD27,230.73360000000002,NGVD29,KY,Perry,LRL,Buckhorn Lake,Buckhorn Lake,Buckhorn Lake,Corps Reservoir,,, +LRL,Kentucky-KYDK2,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-ServiceGate01,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-ServiceGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-ServiceGate01'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Taylorsville-ServiceGate02,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-ServiceGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-ServiceGate02'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Taylorsville-Bypass01,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Bypass01'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Bypass01'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Cairo-CIRI2,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '370000089094501-CIRI2'}]",,,,,,,,,,,,,,,,, +LRL,Olmsted,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600'}]",Presque Isle,America/Chicago,0.0,0.0,,0.0,,,,,,,,,0.0,0.0, +LRL,Taylorsville,PROJECT,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,Taylorsville Lake,Taylorsville Lake,Corps Reservoir,,, +LRL,Dundee-DUNK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03319000-DUNK2L'}]","Narrows, KY",US/Central,37.5475514,-86.7216546,NAD83,393.18,NGVD29,KY,Ohio,LRL,,,,,,, +LRL,HorseBranch-DUNK2S,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03318800-DUNK2S'}]",,,,,,,,,,,,,,,,, +LRL,BowlingGreenKY,SITE,ft,,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE774640'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03314500'}]",,US/Central,37.002819444444,-86.432766666667,,409.83,NGVD29,KY,Warren,,,"BARREN RIVER AT BOWLING GREEN, K","BARREN RIVER AT BOWLING GREEN, KY",,,, +LRL,DUNK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Olmsted-TW,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-TW'}]",Presque Isle,Unknown or Not Applicable,0.0,0.0,,0.0,,00,Unknown County or County N/A for Unknown State or State N/A,,,Olmsted Tailwater,,,-3.4028234663852886e+38,-3.4028234663852886e+38, +LRL,ALVK2S,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-BRCK2L,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-BRCK2L'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-BRCK2L'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,OlmstedUpr,SITE,m,United States,True,[],"Olmsted, IL",US/Central,37.183424262929,-89.063253433899,NAD83,84.86851200000001,NGVD29,KY,Ballard,LRL,Olmsted Dam Upper Gage,OHIO RIVER AT OLMSTED,OHIO RIVER AT OLMSTED,Stream Gauge,,, +LRL,Taylorsville-TAYO1,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-TAYO1'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-TAYO1'}]","Vandalia, OH",US/Eastern,39.8728351,-84.164107,NAD83,760.11,NGVD29,OH,Montgomery,LRL,Taylorsville Dam,Great Miami River at Taylorsvill,Great Miami River at Taylorsville OH,Stream Gauge,,, +LRL,BowlingGreenIN,SITE,m,,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03360000'}, {'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE77835E'}]",,US/Eastern,39.382819444444,-87.020566666667,,167.036496,NGVD29,IN,Clay,,,"EEL RIVER AT BOWLING GREEN, IN","EEL RIVER AT BOWLING GREEN, IN",Stream Gauge,,, +LRL,Newburghwq,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,GreenMouth,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Kentucky,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Cincinnati,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03255000'}]","Covington, KY",US/Eastern,39.0945044,-84.5104983,NAD27,428.87999999999994,NGVD29,OH,Hamilton,LRL,Ohio River at Cincinnati,"OHIO RIVER AT CINCINNATI, OH","OHIO RIVER AT CINCINNATI, OH",Stream Gauge,,, +LRL,Newburgh-NBGI3,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03304300-NBGI3'}]","Newburgh, IN",Unknown or Not Applicable,37.934301333332,-87.379384665112,NAD83,100.434648,NGVD29,00,Unknown County or County N/A for Unknown State or State N/A,LRL,Newburgh Locks and Dam TW,,,,,, +LRL,Olmsted-OLMI2,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-OLMI2'}]",Presque Isle,America/Chicago,0.0,0.0,,0.0,,,,,,,,,0.0,0.0, +LRL,Greensburg-GNSK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03306500-GNSK2L'}]","Greensburg, KY",US/Central,37.2536713,-85.5030215,NAD83,531.81,NGVD29,KY,Green,LRL,,,,,,, +LRL,Barkley,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, diff --git a/load_data/data/.ipynb_checkpoints/LRL_timeseries_ids-checkpoint.csv b/load_data/data/.ipynb_checkpoints/LRL_timeseries_ids-checkpoint.csv new file mode 100755 index 0000000000..eaadbba2a6 --- /dev/null +++ b/load_data/data/.ipynb_checkpoints/LRL_timeseries_ids-checkpoint.csv @@ -0,0 +1,444 @@ +,office,ts_id +0,LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-raw +1,LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-rev +2,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-raw +3,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-rev +4,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-raw +5,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-rev +6,LRL,Taylorsville-Lake.Precip.Inst.15Minutes.0.LRGS-raw +7,LRL,Taylorsville-Lake.Precip.Inst.5Minutes.0.LRGS-raw +8,LRL,Taylorsville-Lake.Precip.Total.5Minutes.5Minutes.USGS-raw +9,LRL,Taylorsville-Lake.Precip.Total.5Minutes.5Minutes.USGS-rev +10,LRL,Taylorsville-Lake.Temp-Water.Inst.15Minutes.0.USGS-raw +11,LRL,Taylorsville-Lake.Temp-Water.Inst.15Minutes.0.USGS-rev +12,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.LRGS-comp +13,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.USGS-raw +14,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.USGS-rev +15,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.LRGS-raw +16,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.LRGS-rev +17,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.USGS-raw +18,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.USGS-rev +19,LRL,BowlingGreenIN.Stage.Inst.~1Day.0.lrldlb-raw +20,LRL,BowlingGreenIN.Stage.Inst.~1Day.0.lrldlb-rev +21,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.LRGS-comp +22,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-raw +23,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-rev +24,LRL,BowlingGreenKY.Precip.Inst.15Minutes.0.LRGS-raw +25,LRL,BowlingGreenKY.Precip.Total.15Minutes.15Minutes.USGS-raw +26,LRL,BowlingGreenKY.Precip.Total.15Minutes.15Minutes.USGS-rev +27,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-raw +28,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-rev +29,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-raw +30,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-rev +31,LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-raw +32,LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-rev +33,LRL,GREENUP-GNUK2.Code-Ice.Inst.0.0.LPMS-raw +34,LRL,GREENUP-GNUK2.Code-Weather.Inst.0.0.LPMS-raw +35,LRL,GREENUP-GNUK2.Count-Hydropower-Units.Inst.0.0.LPMS-raw +36,LRL,GREENUP-GNUK2.Dir-Wind.Inst.0.0.LPMS-raw +37,LRL,GREENUP-GNUK2.Flow-Hydropower.Inst.0.0.LPMS-raw +38,LRL,GREENUP-GNUK2.Flow-Total.Inst.0.0.LPMS-comp +39,LRL,GREENUP-GNUK2.Opening-Gate-Total.Inst.0.0.LPMS-raw +40,LRL,GREENUP-GNUK2.Precip.Total.0.1Day.LPMS-raw +41,LRL,GREENUP-GNUK2.Speed-Wind.Inst.0.0.LPMS-raw +42,LRL,GREENUP-GNUK2.Stage-Headwater.Inst.0.0.LPMS-raw +43,LRL,GREENUP-GNUK2.Stage-Headwater.Inst.1Day.0.LPMS-comp +44,LRL,GREENUP-GNUK2.Stage-Tailwater.Inst.0.0.LPMS-raw +45,LRL,GREENUP-GNUK2.Stage-Tailwater.Inst.1Day.0.LPMS-comp +46,LRL,GREENUP-GNUK2.Temp-Air.Inst.0.0.LPMS-raw +47,LRL,GREENUP-GNUK2.Temp-Water.Inst.0.0.LPMS-raw +48,LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-raw +49,LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-rev +50,LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-raw +51,LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-rev +52,LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-raw +53,LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-rev +54,LRL,Green-Lake.Precip.Inst.15Minutes.0.LRGS-raw +55,LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-raw +56,LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-rev +57,LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-raw +58,LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-rev +59,LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-raw +60,LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-rev +61,LRL,Green.Depth-Snow.Inst.~1Day.0.radar-test +62,LRL,Green.Depth-SnowWE.Inst.~1Day.0.radar-test +63,LRL,Green.Elev.Inst.0.0.lrldlb-raw +64,LRL,Green.Elev.Inst.0.0.lrldlb-rev +65,LRL,Green.Elev.Inst.0.0.radar-test +66,LRL,Green.Elev.Inst.15Minutes.0.LRGS-raw +67,LRL,Green.Elev.Inst.15Minutes.0.LRGS-rev +68,LRL,Green.Elev.Inst.1Hour.0.LRL-cavi-fct +69,LRL,Green.Elev.Inst.1Hour.0.National-CWMS-Forecast +70,LRL,Green.Elev.Inst.1Hour.0.lrldlb-comp +71,LRL,Green.Elev.Inst.~1Day.0.LRL-cavi-fct +72,LRL,Green.Flow-BP1.Inst.0.0.lrldlb-comp +73,LRL,Green.Flow-BP1.Inst.1Hour.0.lrldlb-comp +74,LRL,Green.Flow-BP2.Inst.0.0.lrldlb-comp +75,LRL,Green.Flow-BP2.Inst.1Hour.0.lrldlb-comp +76,LRL,Green.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +77,LRL,Green.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +78,LRL,Green.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +79,LRL,Green.Flow-MG1.Inst.0.0.lrldlb-comp +80,LRL,Green.Flow-MG1.Inst.1Hour.0.lrldlb-comp +81,LRL,Green.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +82,LRL,Green.Flow-Outflow.Inst.0.0.lrldlb-comp +83,LRL,Green.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +84,LRL,Green.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +85,LRL,Green.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +86,LRL,Green.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +87,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-fct +88,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-raw +89,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-rev +90,LRL,Green.Opening-BP1.Inst.0.0.radar-test +91,LRL,Green.Opening-BP1.Inst.1Hour.0.lrldlb-comp +92,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-fct +93,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-raw +94,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-rev +95,LRL,Green.Opening-BP2.Inst.0.0.radar-test +96,LRL,Green.Opening-BP2.Inst.1Hour.0.lrldlb-comp +97,LRL,Green.Opening-L1.Inst.0.0.lrldlb-fct +98,LRL,Green.Opening-L1.Inst.0.0.lrldlb-raw +99,LRL,Green.Opening-L1.Inst.0.0.lrldlb-rev +100,LRL,Green.Opening-L1.Inst.0.0.radar-test +101,LRL,Green.Opening-L1.Inst.1Hour.0.lrldlb-comp +102,LRL,Green.Opening-L2.Inst.0.0.lrldlb-fct +103,LRL,Green.Opening-L2.Inst.0.0.lrldlb-raw +104,LRL,Green.Opening-L2.Inst.0.0.lrldlb-rev +105,LRL,Green.Opening-L2.Inst.0.0.radar-test +106,LRL,Green.Opening-L2.Inst.1Hour.0.lrldlb-comp +107,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-fct +108,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-raw +109,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-rev +110,LRL,Green.Opening-MG1.Inst.0.0.radar-test +111,LRL,Green.Opening-MG1.Inst.1Hour.0.lrldlb-comp +112,LRL,Green.Opening-MG2.Inst.0.0.lrldlb-raw +113,LRL,Green.Opening-MG2.Inst.0.0.lrldlb-rev +114,LRL,Green.Opening-MG2.Inst.1Hour.0.lrldlb-comp +115,LRL,Green.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +116,LRL,Green.Precip.Inst.15Minutes.0.LRGS-raw +117,LRL,Green.Precip.Inst.1Hour.0.National-CWMS-Forecast +118,LRL,Green.Precip.Total.1Hour.1Hour.Openweather +119,LRL,Green.Precip.Total.~1Day.1Day.lrldlb-raw +120,LRL,Green.Precip.Total.~1Day.1Day.lrldlb-rev +121,LRL,Green.Precip.Total.~1Day.1Day.radar-test +122,LRL,Green.Stage-Tailwater.Inst.0.0.lrldlb-raw +123,LRL,Green.Stage-Tailwater.Inst.0.0.radar-test +124,LRL,Green.Stage.Inst.15Minutes.0.LRGS-raw +125,LRL,Green.Stage.Inst.15Minutes.0.LRGS-rev +126,LRL,Green.Stor.Inst.1Hour.0.lrldlb-comp +127,LRL,Green.Temp-Air.Inst.1Hour.0.Openweather +128,LRL,Green.Temp-Air.Inst.~1Day.0.lrldlb-raw +129,LRL,Green.Temp-Air.Inst.~1Day.0.radar-test +130,LRL,Green.Temp-Air.Max.~1Day.1Day.lrldlb-raw +131,LRL,Green.Temp-Air.Max.~1Day.1Day.radar-test +132,LRL,Green.Temp-Air.Min.~1Day.1Day.lrldlb-raw +133,LRL,Green.Temp-Air.Min.~1Day.1Day.radar-test +134,LRL,Green.Temp-Water.Inst.~1Day.0.lrldlb-raw +135,LRL,Green.Temp-Water.Inst.~1Day.0.radar-test +136,LRL,Greensburg.Flow.Inst.15Minutes.0.LRGS-comp +137,LRL,Greensburg.Precip.Inst.15Minutes.0.LRGS-raw +138,LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-raw +139,LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-rev +140,LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-raw +141,LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-rev +142,LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-raw +143,LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-rev +144,LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-raw +145,LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-rev +146,LRL,Greensburg.Stage.Inst.~1Day.0.radar-test +147,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.LRGS-raw +148,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.LRGS-rev +149,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.USGS-raw +150,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.USGS-rev +151,LRL,Catawba.Flow.Inst.15Minutes.0.LRGS-comp +152,LRL,Catawba.Flow.Inst.15Minutes.0.USGS-raw +153,LRL,Catawba.Flow.Inst.15Minutes.0.USGS-rev +154,LRL,Catawba.Flow.Inst.~1Day.0.LRL-cavi-fct +155,LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-raw +156,LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-rev +157,LRL,Catawba.Stage.Inst.15Minutes.0.USGS-raw +158,LRL,Catawba.Stage.Inst.15Minutes.0.USGS-rev +159,LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-raw +160,LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-rev +161,LRL,Buckhorn-Lake.Elev.Inst.15Minutes.0.LRGS-raw +162,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-raw +163,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-rev +164,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-raw +165,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-rev +166,LRL,Buckhorn-Lake.Precip.Inst.15Minutes.0.LRGS-raw +167,LRL,Buckhorn-Lake.Precip.Inst.5Minutes.0.LRGS-raw +168,LRL,Buckhorn-Lake.Precip.Total.5Minutes.5Minutes.USGS-raw +169,LRL,Buckhorn-Lake.Precip.Total.5Minutes.5Minutes.USGS-rev +170,LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-raw +171,LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-rev +172,LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-raw +173,LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-rev +174,LRL,TaylorsvilleOH.Elev.Inst.15Minutes.0.USGS-comp +175,LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-raw +176,LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-rev +177,LRL,TaylorsvilleOH.Precip.Total.15Minutes.15Minutes.USGS-raw +178,LRL,TaylorsvilleOH.Precip.Total.15Minutes.15Minutes.USGS-rev +179,LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-raw +180,LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-rev +181,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +182,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +183,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +184,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +185,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +186,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +187,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +188,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +189,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +190,LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-raw +191,LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-rev +192,LRL,Boston.Flow.Inst.0.0.USGS-raw +193,LRL,Boston.Flow.Inst.0.0.USGS-rev +194,LRL,Boston.Flow.Inst.15Minutes.0.LRGS-comp +195,LRL,Boston.Flow.Inst.15Minutes.0.USGS-raw +196,LRL,Boston.Flow.Inst.15Minutes.0.USGS-rev +197,LRL,Boston.Flow.Inst.~1Day.0.LRL-cavi-fct +198,LRL,Boston.Precip.Inst.15Minutes.0.LRGS-raw +199,LRL,Boston.Precip.Total.15Minutes.15Minutes.USGS-raw +200,LRL,Boston.Precip.Total.15Minutes.15Minutes.USGS-rev +201,LRL,Boston.Stage.Inst.15Minutes.0.LRGS-raw +202,LRL,Boston.Stage.Inst.15Minutes.0.LRGS-rev +203,LRL,Boston.Stage.Inst.15Minutes.0.USGS-raw +204,LRL,Boston.Stage.Inst.15Minutes.0.USGS-rev +205,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +206,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +207,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +208,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +209,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +210,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-operQPF +211,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +212,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +213,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +242,LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-0QPF +243,LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-48QPF +244,LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-0QPF +245,LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-48QPF +246,LRL,Cairo.Elev.Inst.1Hour.0.USGS-rev +247,LRL,Cairo.Precip.Inst.1Hour.0.LRGS-raw +248,LRL,Cairo.Stage.Inst.1Hour.0.LRGS-raw +249,LRL,Cairo.Stage.Inst.1Hour.0.LRGS-rev +250,LRL,Cairo.Stage.Inst.1Hour.0.USGS-raw +251,LRL,Cairo.Stage.Inst.1Hour.0.USGS-rev +252,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp +253,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp-SOModC +254,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp-USGS +255,LRL,Newburgh-NBGI3.Opening-Gate-Total.Inst.0.0.LPMS-raw +256,LRL,Newburgh-NBGI3.Stage-Headwater.Inst.0.0.LPMS-raw +257,LRL,Newburgh-NBGI3.Stage-Headwater.Inst.1Day.0.LPMS-comp +258,LRL,Newburgh-NBGI3.Stage-Tailwater.Inst.0.0.LPMS-raw +259,LRL,Newburgh-NBGI3.Stage-Tailwater.Inst.1Day.0.LPMS-comp +260,LRL,Newburgh.Precip.Inst.15Minutes.0.LRGS-raw +261,LRL,Newburgh.Precip.Total.15Minutes.15Minutes.USGS-raw +262,LRL,Newburgh.Precip.Total.15Minutes.15Minutes.USGS-rev +263,LRL,Newburgh.Stage.Inst.15Minutes.0.LRGS-raw +264,LRL,Newburgh.Stage.Inst.15Minutes.0.LRGS-rev +265,LRL,Newburgh.Stage.Inst.15Minutes.0.USGS-raw +266,LRL,Newburgh.Stage.Inst.15Minutes.0.USGS-rev +267,LRL,Barkley-BARK2.Flow.Ave.~6Hours.6Hours.TVA-fct +268,LRL,Barkley-BARK2.Flow.Inst.1Hour.0.TVA-obs +269,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +270,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +271,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +272,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +273,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +274,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-operQPF +288,LRL,Buckhorn.Depth-Snow.Inst.~1Day.0.radar-test +289,LRL,Buckhorn.Depth-SnowWE.Inst.~1Day.0.radar-test +290,LRL,Buckhorn.Elev.Inst.0.0.lrldlb-raw +291,LRL,Buckhorn.Elev.Inst.0.0.lrldlb-rev +292,LRL,Buckhorn.Elev.Inst.0.0.radar-test +293,LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-raw +294,LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-rev +295,LRL,Buckhorn.Elev.Inst.1Hour.0.LRL-cavi-fct +296,LRL,Buckhorn.Elev.Inst.1Hour.0.National-CWMS-Forecast +297,LRL,Buckhorn.Elev.Inst.1Hour.0.lrldlb-comp +298,LRL,Buckhorn.Elev.Inst.~1Day.0.LRL-cavi-fct +299,LRL,Buckhorn.Flow-BP1.Inst.0.0.lrldlb-comp +300,LRL,Buckhorn.Flow-BP1.Inst.1Hour.0.lrldlb-comp +301,LRL,Buckhorn.Flow-BP2.Inst.0.0.lrldlb-comp +302,LRL,Buckhorn.Flow-BP2.Inst.1Hour.0.lrldlb-comp +303,LRL,Buckhorn.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +304,LRL,Buckhorn.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +305,LRL,Buckhorn.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +306,LRL,Buckhorn.Flow-MG1.Inst.0.0.lrldlb-comp +307,LRL,Buckhorn.Flow-MG1.Inst.1Hour.0.lrldlb-comp +308,LRL,Buckhorn.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +309,LRL,Buckhorn.Flow-Outflow.Inst.0.0.lrldlb-comp +310,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +311,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +312,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +313,LRL,Buckhorn.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +314,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-fct +315,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-raw +316,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-rev +317,LRL,Buckhorn.Opening-BP1.Inst.0.0.radar-test +318,LRL,Buckhorn.Opening-BP1.Inst.1Hour.0.lrldlb-comp +319,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-fct +320,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-raw +321,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-rev +322,LRL,Buckhorn.Opening-BP2.Inst.0.0.radar-test +323,LRL,Buckhorn.Opening-BP2.Inst.1Hour.0.lrldlb-comp +324,LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-raw +325,LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-rev +326,LRL,Buckhorn.Opening-L1.Inst.1Hour.0.lrldlb-comp +327,LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-raw +328,LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-rev +329,LRL,Buckhorn.Opening-L2.Inst.1Hour.0.lrldlb-comp +330,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-fct +331,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-raw +332,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-rev +333,LRL,Buckhorn.Opening-MG1.Inst.0.0.radar-test +334,LRL,Buckhorn.Opening-MG1.Inst.1Hour.0.lrldlb-comp +335,LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-raw +336,LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-rev +337,LRL,Buckhorn.Opening-MG2.Inst.1Hour.0.lrldlb-comp +338,LRL,Buckhorn.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +339,LRL,Buckhorn.Precip.Inst.15Minutes.0.LRGS-raw +340,LRL,Buckhorn.Precip.Inst.1Hour.0.National-CWMS-Forecast +341,LRL,Buckhorn.Precip.Total.1Hour.1Hour.Openweather +342,LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-raw +343,LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-rev +344,LRL,Buckhorn.Precip.Total.~1Day.1Day.radar-test +345,LRL,Buckhorn.Stage-Tailwater.Inst.0.0.lrldlb-raw +346,LRL,Buckhorn.Stage-Tailwater.Inst.0.0.radar-test +347,LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-raw +348,LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-rev +349,LRL,Buckhorn.Stor.Inst.1Hour.0.lrldlb-comp +350,LRL,Buckhorn.Temp-Air.Inst.1Hour.0.Openweather +351,LRL,Buckhorn.Temp-Air.Inst.~1Day.0.lrldlb-raw +352,LRL,Buckhorn.Temp-Air.Inst.~1Day.0.radar-test +353,LRL,Buckhorn.Temp-Air.Max.~1Day.1Day.lrldlb-raw +354,LRL,Buckhorn.Temp-Air.Max.~1Day.1Day.radar-test +355,LRL,Buckhorn.Temp-Air.Min.~1Day.1Day.lrldlb-raw +356,LRL,Buckhorn.Temp-Air.Min.~1Day.1Day.radar-test +357,LRL,Buckhorn.Temp-Water.Inst.~1Day.0.lrldlb-raw +358,LRL,Buckhorn.Temp-Water.Inst.~1Day.0.radar-test +359,LRL,Kentucky-KYDK2.Flow.Ave.~6Hours.6Hours.TVA-fct +360,LRL,Kentucky-KYDK2.Flow.Inst.1Hour.0.TVA-obs +367,LRL,Olmsted-OLMI2.Code-Ice.Inst.0.0.LPMS-raw +368,LRL,Olmsted-OLMI2.Code-Weather.Inst.0.0.LPMS-raw +369,LRL,Olmsted-OLMI2.Count-Needles.Inst.0.0.LPMS-raw +370,LRL,Olmsted-OLMI2.Count-Wickets-Down.Inst.0.0.LPMS-raw +371,LRL,Olmsted-OLMI2.Dir-Wind.Inst.0.0.LPMS-raw +372,LRL,Olmsted-OLMI2.Elev-Headwater.Inst.15Minutes.0.USGS-comp +373,LRL,Olmsted-OLMI2.Elev-Tailwater.Inst.15Minutes.0.USGS-comp +374,LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-raw +375,LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-rev +376,LRL,Olmsted-OLMI2.Opening-Gate-Average.Inst.1Hour.0.LPMS-comp +377,LRL,Olmsted-OLMI2.Opening-Gate-Total.Inst.0.0.LPMS-raw +378,LRL,Olmsted-OLMI2.Precip.Total.0.1Day.LPMS-raw +379,LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-raw +380,LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-rev +381,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.0.0.LPMS-raw +382,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-raw +383,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-rev +384,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.1Day.0.LPMS-comp +385,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.0.0.LPMS-raw +386,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-raw +387,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-rev +388,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.1Day.0.LPMS-comp +389,LRL,Olmsted-OLMI2.Temp-Air.Inst.0.0.LPMS-raw +390,LRL,Olmsted-OLMI2.Temp-Water.Inst.0.0.LPMS-raw +391,LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-raw +392,LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-rev +393,LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-raw +394,LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-rev +395,LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-raw +396,LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-rev +397,LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-raw +398,LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-rev +399,LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-raw +400,LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-rev +401,LRL,Olmsted.Temp-Water.Inst.15Minutes.0.USGS-raw +402,LRL,Olmsted.Temp-Water.Inst.15Minutes.0.USGS-rev +403,LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-raw +404,LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-rev +417,LRL,Taylorsville.Elev.Inst.0.0.lrldlb-raw +418,LRL,Taylorsville.Elev.Inst.0.0.lrldlb-rev +419,LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-raw +420,LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-rev +421,LRL,Taylorsville.Elev.Inst.1Hour.0.LRL-cavi-fct +422,LRL,Taylorsville.Elev.Inst.1Hour.0.National-CWMS-Forecast +423,LRL,Taylorsville.Elev.Inst.1Hour.0.lrldlb-comp +424,LRL,Taylorsville.Elev.Inst.~1Day.0.LRL-cavi-fct +425,LRL,Taylorsville.Flow-BP1.Inst.0.0.lrldlb-comp +426,LRL,Taylorsville.Flow-BP1.Inst.1Hour.0.lrldlb-comp +427,LRL,Taylorsville.Flow-BP2.Inst.0.0.lrldlb-comp +428,LRL,Taylorsville.Flow-BP2.Inst.1Hour.0.lrldlb-comp +429,LRL,Taylorsville.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +430,LRL,Taylorsville.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +431,LRL,Taylorsville.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +432,LRL,Taylorsville.Flow-MG1.Inst.0.0.lrldlb-comp +433,LRL,Taylorsville.Flow-MG1.Inst.1Hour.0.lrldlb-comp +434,LRL,Taylorsville.Flow-MG2.Inst.0.0.lrldlb-comp +435,LRL,Taylorsville.Flow-MG2.Inst.1Hour.0.lrldlb-comp +436,LRL,Taylorsville.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +437,LRL,Taylorsville.Flow-Outflow.Inst.0.0.lrldlb-comp +438,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +439,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +440,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +441,LRL,Taylorsville.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +442,LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-raw +443,LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-rev +444,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-fct +445,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-raw +446,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-rev +447,LRL,Taylorsville.Opening-BP1.Inst.1Hour.0.lrldlb-comp +448,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-fct +449,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-raw +450,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-rev +451,LRL,Taylorsville.Opening-BP2.Inst.1Hour.0.lrldlb-comp +452,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-fct +453,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-raw +454,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-rev +455,LRL,Taylorsville.Opening-L1.Inst.1Hour.0.lrldlb-comp +456,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-fct +457,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-raw +458,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-rev +459,LRL,Taylorsville.Opening-L2.Inst.1Hour.0.lrldlb-comp +460,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-fct +461,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-raw +462,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-rev +463,LRL,Taylorsville.Opening-MG1.Inst.1Hour.0.lrldlb-comp +464,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-fct +465,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-raw +466,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-rev +467,LRL,Taylorsville.Opening-MG2.Inst.1Hour.0.lrldlb-comp +468,LRL,Taylorsville.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +469,LRL,Taylorsville.Precip.Inst.15Minutes.0.LRGS-raw +470,LRL,Taylorsville.Precip.Inst.1Hour.0.National-CWMS-Forecast +471,LRL,Taylorsville.Precip.Total.15Minutes.15Minutes.USGS-raw +472,LRL,Taylorsville.Precip.Total.15Minutes.15Minutes.USGS-rev +473,LRL,Taylorsville.Precip.Total.1Hour.1Hour.Openweather +474,LRL,Taylorsville.Precip.Total.~1Day.1Day.lrldlb-raw +475,LRL,Taylorsville.Precip.Total.~1Day.1Day.lrldlb-rev +476,LRL,Taylorsville.Stage-Tailwater.Inst.0.0.lrldlb-raw +477,LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-raw +478,LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-rev +479,LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-raw +480,LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-rev +481,LRL,Taylorsville.Stor.Inst.1Hour.0.lrldlb-comp +482,LRL,Taylorsville.Temp-Air.Inst.1Hour.0.Openweather +483,LRL,Taylorsville.Temp-Air.Inst.~1Day.0.lrldlb-raw +484,LRL,Taylorsville.Temp-Air.Max.~1Day.1Day.lrldlb-raw +485,LRL,Taylorsville.Temp-Air.Min.~1Day.1Day.lrldlb-raw +486,LRL,Taylorsville.Temp-Water.Inst.~1Day.0.lrldlb-raw +506,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +507,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +508,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +509,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +510,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +511,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-operQPF +533,LRL,Cincinnati.Elev.Inst.15Minutes.0.USGS-comp +534,LRL,Cincinnati.Flow.Inst.15Minutes.0.USGS-raw +535,LRL,Cincinnati.Flow.Inst.15Minutes.0.USGS-rev +536,LRL,Cincinnati.Stage.Inst.15Minutes.0.LRGS-raw +537,LRL,Cincinnati.Stage.Inst.15Minutes.0.LRGS-rev +538,LRL,Cincinnati.Stage.Inst.15Minutes.0.USGS-raw +539,LRL,Cincinnati.Stage.Inst.15Minutes.0.USGS-rev +540,LRL,Cincinnati.Stage.Inst.~1Day.0.lrldlb-raw +541,LRL,Cincinnati.Stage.Inst.~1Day.0.lrldlb-rev diff --git a/load_data/data/.ipynb_checkpoints/MVP_locations_data-checkpoint.csv b/load_data/data/.ipynb_checkpoints/MVP_locations_data-checkpoint.csv new file mode 100755 index 0000000000..10b2d6e408 --- /dev/null +++ b/load_data/data/.ipynb_checkpoints/MVP_locations_data-checkpoint.csv @@ -0,0 +1,171 @@ +office,name,nearest-city,public-name,kind,time-zone,latitude,longitude,unit,nation,state,county,bounding-office,active,aliases,long-name,map-label,type,published-latitude,published-longitude,horizontal-datum,elevation,vertical-datum,description +MVP,LockDam_05-TainterGate23,Winona,Lock and Dam 05 Tainter Gate 23,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate23'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate23'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate23'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate23'}]",Mississippi River Lock and Dam 05 Tainter Gate 23,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,TraverseWR_Dam-MainLake,Fergus Falls,"Mud Lake North of 670TH Street Near Wheaton, MN",SITE,US/Central,45.836055555556,-96.572944444444,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-MainLake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-MainLake'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-MainLake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049900'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-MainLake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-MainLake'}]",White Rock Dam Main Lake Gage,White Rock Dam Main Lake Gage,,0.0,0.0,NAD83,899.9999999999999,LOCAL,"MUD LAKE NORTH OF 670TH STREET NEAR WHEATON, MN" +MVP,Highway75_Dam,Odessa,Highway 75 Dam,PROJECT,US/Central,45.248301,-96.29788,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam'}]",Highway 75 Dam at Big Stone Lake and Whetstone River Project,Highway 75 Dam,Dam,0.0,0.0,NAD83,0.0,NGVD29,USACE Owned and Maintained +MVP,Baldhill_Dam-ServiceSpillway,Jamestown,Baldhill Dam Service Spillway,SITE,US/Central,47.0356524,-98.0813989,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-ServiceSpillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-ServiceSpillway'}]",,Baldhill Dam Service Spillway,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-SlideGate01,Brainerd,Gull Slide Gate 01,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate01'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate25,Winona,Lock and Dam 05 Tainter Gate 25,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate25'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate25'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate25'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate25'}]",Mississippi River Lock and Dam 05 Tainter Gate 25,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-Tailwater,Winona,Lock and Dam 05a Tailwater,SITE,US/Central,44.085038,-91.667549,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05378490'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Tailwater'}]",Mississippi River Lock and Dam 05a Tailwater,Mississippi River Lock and Dam 05a Tailwater,,0.0,0.0,NAD83,0.0,NAVD88, +MVP,LockDam_05-TainterGate27,Winona,Lock and Dam 05 Tainter Gate 27,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate27'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate27'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate27'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate27'}]",Mississippi River Lock and Dam 05 Tainter Gate 27,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-CP_LD05TW,Winona,Lock Dam 05a Ctrl Point,SITE,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-CP_LD05TW'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-CP_LD05TW'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-CP_LD05TW'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-CP_LD05TW'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ZUMM5,Rochester,Zumbro River at Zumbro Falls,SITE,US/Central,44.2867,-92.4322,m,United States,MN,Wabasha,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05374000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ZUMM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ZUMM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ZUMM5'}]","Zumbro River at Zumbro Falls, MN",,DCP Gage,,,,247.272048,LOCAL, +MVP,MissHW_PineRiver-SlideGate04,Brainerd,Pine River Dam Slide Gate 04,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate04'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-TainterGate01,Fergus Falls,White Rock Dam Tainter Gate 01,OUTLET,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate01'}]",,White Rock Dam Tainter Gate,,,,NAD83,274.32,LOCAL, +MVP,Highway75_Dam-LowFlow,Watertown,Highway 75 Dam Low Flow,OUTLET,US/Central,45.248536111111,-96.297127777778,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LowFlow'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LowFlow'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LowFlow'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LowFlow'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ODAM5'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LFKM5S,Hibbing,Little Fork Snow,SITE,US/Central,48.39861,-93.56722,ft,United States,MN,Koochiching,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LFKM5S'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LFKM5S'}]",Little Fork Snow,Little Fork Snow,,,,,,, +MVP,LockDam_05-TainterGates,Winona,Lock and Dam 05 Tainter Gates,SITE,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGates'}]",Mississippi River Lock and Dam 05 Tainter Gates,Mississippi River Lock and Dam 05 Tainter Gates,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-RollerGate01,Winona,Lock and Dam 05a Roller Gate 01,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate01'}]",Mississippi River Lock and Dam 05a Roller Gate 01,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate02,Brainerd,Pine River Dam Slide Gate 02,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate02'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,TraverseWR_Dam-TainterGate03,Fergus Falls,White Rock Dam Tainter Gate 03,OUTLET,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate03'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate03'}]",,White Rock Dam Tainter Gate,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_02-HydroTurbines,Hastings,Lock and Dam 02 Hydro Turbines,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-HydroTurbines'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-HydroTurbines'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-HydroTurbines'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-HydroTurbines'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver,Crosslake,Pine River Dam at Cross Lake,PROJECT,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver'}]",Mississippi River Headwaters Pine River Dam at Cross Lake,Pine River Dam at Cross Lake,Dam,,,NAD83,1199.9999999999998,NGVD29,USACE Owned and Maintained +MVP,LockDam_02-TainterValves,Hastings,Lock and Dam 02 Tainter Valves,SITE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterValves'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterValves'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate13,Winona,Lock and Dam 05 Tainter Gate 13,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate13'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate13'}]",Mississippi River Lock and Dam 05 Tainter Gate 13,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_Gull-Lake,Brainerd,Gull Lake near Brainerd,SITE,US/Central,46.410931,-94.359777,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLKM5'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Lake'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Lake'}]",,Gull Lake Dam,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-Tailwater,Brainerd,Gull Lake Dam Tailwater,SITE,US/Central,46.410869,-94.353622,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Tailwater'}]",,Gull Lake Dam,,,,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate15,Winona,Lock and Dam 05 Tainter Gate 15,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate15'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate15'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate15'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate15'}]",Mississippi River Lock and Dam 05 Tainter Gate 15,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_Gull-SlideGate03,Brainerd,Gull Slide Gate 03,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate03'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate17,Winona,Lock and Dam 05 Tainter Gate 17,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate17'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate17'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate17'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate17'}]",Mississippi River Lock and Dam 05 Tainter Gate 17,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Highway75_Dam-LowFlow-Tailwater,Watertown,Highway 75 Dam Low Flow Tailwater,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LowFlow-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LowFlow-Tailwater'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LowFlow-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LowFlow-Tailwater'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate19,Winona,Lock and Dam 05 Tainter Gate 19,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate19'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate19'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate19'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate19'}]",Mississippi River Lock and Dam 05 Tainter Gate 19,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,DAWM5,Marshall,W Br Lac Qui Parle nr Dawson,SITE,US/Central,44.9297,-96.0514,ft,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'Agency Aliases-MNDNR', 'value': '24059001'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'DAWM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'DAWM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'DAWM5'}]","West Branch Lac Qui Parle River near Dawson, MN",,DCP Gage,,,,,,Owner is MNDNR +MVP,Highway75_Dam-Tailwater,Watertown,Highway 75 Dam Tailwater,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-Tailwater'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-Tailwater'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-SlideGate05,Brainerd,Gull Slide Gate 05,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate05'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_05-TainterGate21,Winona,Lock and Dam 05 Tainter Gate 21,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate21'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate21'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate21'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate21'}]",Mississippi River Lock and Dam 05 Tainter Gate 21,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate13,Cottage Grove,Lock and Dam 02 Tainter Gate 13,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate13'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate32,Winona,Lock and Dam 05 Tainter Gate 32,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate32'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate32'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate32'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate32'}]",Mississippi River Lock and Dam 05 Tainter Gate 32,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-FishPondSiphon,Jamestown,Baldhill Dam Fish Pond Outlet,OUTLET,US/Central,47.033886,-98.0771,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-FishPondSiphon'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-FishPondSiphon'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-FishPondSiphon'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-FishPondSiphon'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-FishPondSiphon'}]",,Baldhill Dam Fish Pond Outlet,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate33,Winona,Lock and Dam 05 Tainter Gate 33,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate33'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate33'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate33'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate33'}]",Mississippi River Lock and Dam 05 Tainter Gate 33,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate15,Hastings,Lock and Dam 02 Tainter Gate 15,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate15'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate15'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate15'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate15'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-Tailwater,Valley City,Baldhill Dam Tailwater,SITE,US/Central,47.032919,-98.083911,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BHTN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05058000'}]",Baldhill Dam Tailwater,Baldhill Dam Tailwater,,0.0,0.0,NAD83,365.76,NGVD29, +MVP,LockDam_02-TainterGate17,Hastings,Lock and Dam 02 Tainter Gate 17,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate17'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate17'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate17'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate17'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate34,Winona,Lock and Dam 05 Tainter Gate 34,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate34'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate34'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate34'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate34'}]",Mississippi River Lock and Dam 05 Tainter Gate 34,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate29,Winona,Lock and Dam 05 Tainter Gate 29,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate29'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate29'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate29'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate29'}]",Mississippi River Lock and Dam 05 Tainter Gate 29,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate07,Winona,Lock and Dam 05 Tainter Gate 07,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate07'}]",Mississippi River Lock and Dam 05 Tainter Gate 07,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate30,Winona,Lock and Dam 05 Tainter Gate 30,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate30'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate30'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate30'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate30'}]",Mississippi River Lock and Dam 05 Tainter Gate 30,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate19,Hastings,Lock and Dam 02 Tainter Gate 19,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate19'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate19'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate19'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate19'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Clayton,Clayton,"Mississippi River at Clayton, IA",STREAM_LOCATION,America/Chicago,42.903611111111,-91.145,m,United States,IA,Clayton,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05411500'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'CLAI4'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CLAI4'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'CLAI4'}]","Mississippi River at Clayton, IA (CP10)","Mississippi River at Clayton, IA",DCP Gage,,,NAD83,600.0,LOCAL, +MVP,Baldhill_Dam-TainterGate02,Jamestown,Baldhill Dam Tainter Gate 02,OUTLET,US/Central,47.0361844,-98.081466,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate02'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate02'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,Highway75_Dam-LeafGate,Watertown,Highway 75 Dam Leaf Gate,OUTLET,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LeafGate'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LeafGate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LeafGate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LeafGate'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate09,Winona,Lock and Dam 05 Tainter Gate 09,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate09'}]",Mississippi River Lock and Dam 05 Tainter Gate 09,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate31,Winona,Lock and Dam 05 Tainter Gate 31,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate31'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate31'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate31'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate31'}]",Mississippi River Lock and Dam 05 Tainter Gate 31,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate11,Winona,Lock and Dam 05 Tainter Gate 11,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate11'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate11'}]",Mississippi River Lock and Dam 05 Tainter Gate 11,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Highway75_Dam-EmergencySpillway,Watertown,,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-EmergencySpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-EmergencySpillway'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-EmergencySpillway'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05a-TainterValves,Winona,Lock and Dam 05a Tainter Valves,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterValves'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterValves'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-RollerGate04,Winona,Lock and Dam 05a Roller Gate 04,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate04'}]",Mississippi River Lock and Dam 05a Roller Gate 04,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05,Minnesota City,Lock and Dam 5,PROJECT,US/Central,44.1609167,-91.8142361,m,United States,MN,Winona,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589'}]",Lock and Dam 05 at Mississippi River 9 foot Channel Navigation Project,Lock and Dam 05,Dam,0.0,0.0,NAD83,182.88,LOCAL,USACE Owned and Maintained +MVP,MissHW_PineRiver-Bays05_06,Brainerd,Pine River Dam Bays 5 & 6 - Slide Gates,SITE,US/Central,46.66914,-94.112745,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays05_06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays05_06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays05_06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays05_06'}]",Pine River Dam Bays 5 & 6 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 5, 6" +MVP,MissHW_PineRiver-SlideGate10,Brainerd,Pine River Dam Slide Gate 10,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate10'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_02-Turbine01,Hastings,Lock and Dam 02 Turbine 01,TURBINE,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Turbine01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Turbine01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Turbine01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Turbine01'}]",,Mississippi River Lock and Dam 02 Turbine 01,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-TainterGate08,Winona,Lock and Dam 05a Tainter Gate 08,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate08'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Lock_02,Hastings,Lock and Dam 02 Lock 02,LOCK,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Lock_02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Lock_02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Lock_02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Lock_02'}]",,Mississippi River Lock and Dam 02,Lock,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-TainterGate09,Winona,Lock and Dam 05a Tainter Gate 09,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate09'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate09'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate03,Hastings,Lock and Dam 02 Tainter Gate 03,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate03'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate08,Brainerd,Pine River Dam Slide Gate 08,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate08'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05a-Lock_01,Winona,Lock Dam 05a Lock 01,LOCK,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Lock_01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Lock_01'}]",,Mississippi River Lock and Dam 05a,Lock,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate05,Hastings,Lock and Dam 02 Tainter Gate 05,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate05'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-Bays01_02,Brainerd,Pine River Dam Bays 1 & 2 - Slide Gates,SITE,US/Central,46.669194,-94.112572,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays01_02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays01_02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays01_02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays01_02'}]",Pine River Dam Bays 1 & 2 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD 83,0.0,NGVD29,"Total opening of gates in bays 1, 2" +MVP,LockDam_02-TainterGates,Hastings,Lock and Dam 02 Tainter Gates,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGates'}]",,Mississippi River Lock and Dam 02 Tainter Gates,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate01,Winona,Lock and Dam 05 Roller Gate 01,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate01'}]",Mississippi River Lock and Dam 05 Roller Gate 01,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate06,Brainerd,Pine River Dam Slide Gate 06,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate06'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,Baldhill_Dam,Valley City,Baldhill Dam at Lake Ashtabula,PROJECT,US/Central,47.0361833,-98.0814667,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'ND00309'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam'}]","Baldhill Dam at Lake Ashtabula near Valley City, ND",Baldhill Dam,Dam,,,NAD83,0.0,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05a-TainterGate10,Winona,Lock and Dam 05a Tainter Gate 10,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate10'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate10'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate07,Hastings,Lock and Dam 02 Tainter Gate 07,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate07'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate07'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate09,Hastings,Lock and Dam 02 Tainter Gate 09,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate09'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate09'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-Powerhouse,Hastings,Lock and Dam 02 Powerhouse,SITE,US/Central,44.760148,-92.867056,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Powerhouse'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Powerhouse'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Powerhouse'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Powerhouse'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,1968.5039370078734,NAVD88, +MVP,Rafferty_Dam-Tailwater,Williston,Rafferty Dam Tailwater,SITE,US/Central,49.1433333,-103.0830556,ft,Canada,00,Unknown County or County N/A for Unknown State or State N/A,,True,"[{'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB032-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'RAFQ8-Tailwater'}, {'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB036'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Rafferty_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Rafferty_Dam-Tailwater'}]","Souris River below Rafferty Reservoir, SK",Souris River b Rafferty RSVR,DCP Gage,,,,,,Owner is Environment Canada +MVP,LockDam_05a-TainterGate06,Winona,Lock and Dam 05a Tainter Gate 06,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate06'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-RollerGate03,Winona,Lock and Dam 05 Roller Gate 03,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate03'}]",Mississippi River Lock and Dam 05 Roller Gate 03,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Cooperstown200,Cooperstown,"SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN, ND",SITE,US/Central,,,ft,United States,ND,Griggs,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Cooperstown2'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05056995'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CPPN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Cooperstown2'}]","SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN, ND",SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN,DCP Gage,,,NAD83,1272.9396325459318,NAVD88,"USACE Owned, USGS Maintained" +MVP,LockDam_05a-RollerGates,Winona,Lock and Dam 05a Roller Gates,SITE,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGates'}]",Mississippi River Lock and Dam 05a Roller Gates,Mississippi River Lock and Dam 05a Roller Gates,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-TainterGate07,Winona,Lock and Dam 05a Tainter Gate 07,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate07'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate05,Winona,Lock and Dam 05 Roller Gate 05,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate05'}]",Mississippi River Lock and Dam 05 Roller Gate 05,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-SlideGate12,Brainerd,Pine River Dam Slide Gate 12,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate12'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate12'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,Highway75_Dam-ServiceSpillway-Gate,Watertown,,OUTLET,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-ServiceSpillway-Gate'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-ServiceSpillway-Gate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-ServiceSpillway-Gate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-ServiceSpillway-Gate'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,ChippewaDiv_Dam-LowFlow,Watson,Chippewa Diversion Dam Low Flow,OUTLET,US/Central,45.021908,-95.792215,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-LowFlow'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578-LowFlow'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-LowFlow'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-LowFlow'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-LowFlow'}]",Chippewa Diversion Dam Low Flow Outlet,Chippewa Diversion Dam Low Flow,Gate,0.0,0.0,NAD83,0.0,NGVD29, +MVP,MissHW_PineRiver-Bays09_10,Brainerd,Pine River Dam Bays 9 & 10 - Slide Gates,SITE,US/Central,46.669089,-94.112926,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays09_10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays09_10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays09_10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays09_10'}]",Pine River Dam Bays 9 & 10 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 9, 10" +MVP,LockDam_02-TainterGate11,Hastings,Lock and Dam 02 Tainter Gate 11,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate11'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate11'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,TraverseWR_Dam-Stoplogs,Fergus Falls,White Rock Dam Stoplogs,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Stoplogs'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Stoplogs'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Stoplogs'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Stoplogs'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Stoplogs'}]",White Rock Dam Stoplogs,White Rock Dam Stoplogs,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_05-TainterGate24,Winona,Lock and Dam 05 Tainter Gate 24,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate24'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate24'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate24'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate24'}]",Mississippi River Lock and Dam 05 Tainter Gate 24,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ChippewaDiv_Dam-TainterGate,Watson,,OUTLET,US/Central,45.0236111,-95.7902778,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00578-TainterGate'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-TainterGate'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-TainterGate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-TainterGate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-TainterGate'}]",,Chippewa Diversion Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate26,Winona,Lock and Dam 05 Tainter Gate 26,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate26'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate26'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate26'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate26'}]",Mississippi River Lock and Dam 05 Tainter Gate 26,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,DAWM5S,Marshall,Dawson Snow,SITE,US/Central,44.82,-96.08,m,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'DAWM5S'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'DAWM5S'}]",Dawson Snow,Dawson Snow,,,,,,, +MVP,LockDam_05-TainterGate28,Winona,Lock and Dam 05 Tainter Gate 28,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate28'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate28'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate28'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate28'}]",Mississippi River Lock and Dam 05 Tainter Gate 28,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ELZM5,Elizabeth,Otter Tail River abv Elizabeth,SITE,US/Central,46.369444444444,-96.017222222222,m,United States,MN,Otter Tail,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05030500'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ELZM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ELZM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ELZM5'}]","Otter Tail River near Elizabeth, MN",,DCP Gage,,,NAD83,381.0,LOCAL,"Otter Tail River near Elizabeth, MN" +MVP,LockDam_05-RollerGates,Winona,Lock and Dam 05 Roller Gates,SITE,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGates'}]",Mississippi River Lock and Dam 05 Roller Gates,Mississippi River Lock and Dam 05 Roller Gates,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate03,Brainerd,Pine River Dam Slide Gate 03,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate03'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-Lake,Fergus Falls,White Rock Dam Lake Gage,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Lake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Lake'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Lake'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Lake'}]",White Rock Dam Lake Gage,White Rock Dam Lake Gage,,0.0,0.0,NAD83,899.9999999999999,LOCAL, +MVP,MissHW_PineRiver-SlideGate01,Brainerd,Pine River Dam Slide Gate 01,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate01'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-TainterGate02,Fergus Falls,White Rock Dam Tainter Gate 02,OUTLET,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate02'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate02'}]",,White Rock Dam Tainter Gate,,,,NAD83,274.32,LOCAL, +MVP,LockDam_02-TainterGate01,Hastings,Lock and Dam 02 Tainter Gate 01,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate01'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-RollerGate02,Winona,Lock and Dam 05a Roller Gate 02,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate02'}]",Mississippi River Lock and Dam 05a Roller Gate 02,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Baldhill_Dam-LowFlow02,Valley City,Baldhill Dam Low Flow Gate 02,OUTLET,US/Central,47.0358647,-98.0810972,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-LowFlow02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-LowFlow02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-LowFlow02'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-LowFlow02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-LowFlow02'}]",Baldhill Dam Low Flow Gate 01,Baldhill Dam Low Flow Gate,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate12,Winona,Lock and Dam 05 Tainter Gate 12,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate12'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate12'}]",Mississippi River Lock and Dam 05 Tainter Gate 12,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ChippewaDiv_Dam-Tailwater,Watson,Chippewa Diversion Dam Tailwater,SITE,US/Central,45.0236111,-95.7902778,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05305000'}]",,Chippewa Diversion Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate14,Winona,Lock and Dam 05 Tainter Gate 14,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate14'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate14'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate14'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate14'}]",Mississippi River Lock and Dam 05 Tainter Gate 14,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,TraverseWR_Dam,Wheaton,White Rock Dam at Mud Lake Reservoir,PROJECT,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577'}]",White Rock Dam at Mud Lake Reservoir at Lake Traverse Project,White Rock Dam at Mud Lake Reservoir,Dam,,,NAD83,274.32,LOCAL,"USACE Owned, USGS Maintained" +MVP,Cooperstown,Jamestown,"Sheyenne River at Cooperstown, ND",SITE,US/Central,47.4336,-98.0286,m,United States,ND,Griggs,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Cooperstown'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CPRN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Cooperstown'}]","Sheyenne River at Cooperstown, ND",,DCP Gage,,,NAD27,387.632448,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05-TainterGate16,Winona,Lock and Dam 05 Tainter Gate 16,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate16'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate16'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate16'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate16'}]",Mississippi River Lock and Dam 05 Tainter Gate 16,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MOOM5S,Cloquet,Moose Lake Snow,SITE,US/Central,46.43639,-92.70611,m,United States,MN,Kanabec,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MOOM5S'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MOOM5S'}]",Moose Lake Snow,Moose Lake Snow,,,,,,, +MVP,ChippewaDiv_Dam,Watson,Chippewa Diversion Dam,PROJECT,US/Central,45.022025,-95.791308333333,m,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578'}]",,Chippewa Diversion Dam,Dam,0.0,0.0,NAD83,0.0,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05-TainterGate18,Winona,Lock and Dam 05 Tainter Gate 18,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate18'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate18'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate18'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate18'}]",Mississippi River Lock and Dam 05 Tainter Gate 18,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Muscoda,Middleton,"Wisconsin River at Muscoda, WI",SITE,US/Central,43.1983,-90.4406,m,United States,WI,Grant,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05407000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MUSW3'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MUSW3'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MUSW3'}]","Wisconsin River at Muscoda, WI","Wisconsin River at Muscoda, WI",DCP Gage,,,NAD83,203.231496,LOCAL, +MVP,LockDam_05-Tailwater,Winona,Lock and Dam 5 Tailwater,SITE,US/Central,44.158307,-91.808447,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-Tailwater'}]",Mississippi River Lock and Dam 05 Tailwater,Mississippi River Lock and Dam 05 Tailwater,,0.0,0.0,NAD83,0.0,NAVD88, +MVP,MissHW_Gull-SlideGate04,Brainerd,Gull Slide Gate 04,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate04'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,MissHW_Gull-SlideGate02,Brainerd,Gull Slide Gate 02,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate02'}]",,Gull Lake Dam,,0.0,,NAD83,0.0,NGVD29, +MVP,Baldhill_Dam-EmergencySpillway,Jamestown,Baldhill Dam Emergency Spillway,SITE,US/Central,47.0346415,-98.0788482,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-EmergencySpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-EmergencySpillway'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-EmergencySpillway'}]",,Baldhill Dam Emergency Spillway,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-StopLog01,Brainerd,Gull Stop Log 01,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-StopLog01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-StopLog01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-StopLog01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-StopLog01'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_05-TainterGate20,Winona,Lock and Dam 05 Tainter Gate 20,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate20'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate20'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate20'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate20'}]",Mississippi River Lock and Dam 05 Tainter Gate 20,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-Spillway,Hastings,Lock and Dam 02 Spillway,SITE,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Spillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Spillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Spillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Spillway'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate22,Winona,Lock and Dam 05 Tainter Gate 22,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate22'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate22'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate22'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate22'}]",Mississippi River Lock and Dam 05 Tainter Gate 22,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,NWUM5,New Ulm,Cottonwood River at New Ulm,SITE,US/Central,44.2914,-94.44,m,United States,MN,Brown,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'NWUM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'NWUM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05317000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'NWUM5'}]",Cottonwood River at New Ulm. MN,,DCP Gage,,,,242.87378400000003,LOCAL, +MVP,NWUM5S,New Ulm,New Ulm Snow,SITE,US/Central,44.32111,-94.43889,m,United States,MN,Brown,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'NWUM5S'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'NWUM5S'}]",New Ulm Snow,New Ulm Snow,,,,,,, +MVP,LockDam_02-TainterGate12,Hastings,Lock and Dam 02 Tainter Gate 12,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate12'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate12'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,TraverseWR_Dam-Tailwater,Fergus Falls,White Rock Dam Tailwater,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05050000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Tailwater'}]",White Rock Dam Tailwater,White Rock Dam Tailwater,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_02-TainterGate14,Hastings,Lock and Dam 02 Tainter Gate 14,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate14'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate14'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate14'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate14'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,AGYM5,Argyle,Middle River at Argyle,SITE,US/Central,48.340277777778,-96.816111111111,ft,United States,MN,Marshall,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'AGYM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'AGYM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'AGYM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05087500'}]","Middle River at Argyle, MN",,DCP Gage,,,NAD83,828.5299999999999,LOCAL,"Middle River at Argyle, MN" +MVP,LockDam_02-TainterGate16,Hastings,Lock and Dam 02 Tainter Gate 16,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate16'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate16'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate16'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate16'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate18,Hastings,Lock and Dam 02 Tainter Gate 18,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate18'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate18'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate18'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate18'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-CP_Alma,Winona,Lock and Dam 05 Control Point,SITE,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-CP_Alma'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-CP_Alma'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-CP_Alma'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-CP_Alma'}]",,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LFKM5,Hibbing,Little Fork River at Little Fk,SITE,US/Central,48.3958,-93.5492,ft,United States,MN,Koochiching,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LFKM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'LFKM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05131500'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LFKM5'}]","Little Fork River at Little Fork, MN",,DCP Gage,,,,1083.59,LOCAL, +MVP,LockDam_05a-Spillway,Winona,Lock and Dam 05a Spillway,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Spillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Spillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Spillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Spillway'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate08,Winona,Lock and Dam 05 Tainter Gate 08,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate08'}]",Mississippi River Lock and Dam 05 Tainter Gate 08,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate10,Winona,Lock and Dam 05 Tainter Gate 10,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate10'}]",Mississippi River Lock and Dam 05 Tainter Gate 10,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-TainterGate01,Jamestown,Baldhill Dam Tainter Gate 01,OUTLET,US/Central,47.0361844,-98.081466,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate01'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,ABRN8,Abercrombie,Wild Rice River at Abercrombie,STREAM_LOCATION,US/Central,46.4680556,-96.7833333,ft,United States,ND,Richland,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ABRN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ABRN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05053000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ABRN8'}]","Wild Rice River near Abercrombie, ND",,,,,NAD83,907.9399999999999,,"Wild Rice River near Abercrombie, ND" +MVP,Baldhill_Dam-LowFlow01,Valley City,Baldhill Dam Low Flow Gate 01,OUTLET,US/Central,47.0359542,-98.0812555,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-LowFlow01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-LowFlow01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-LowFlow01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-LowFlow01'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-LowFlow01'}]",Baldhill Dam Low Flow Gate 01,Baldhill Dam Low Flow Gate 01,,,,NAD83,365.76,NGVD29, +MVP,LockDam_02-Tailwater,Hastings,Lock and Dam 2 Tailwater,SITE,US/Central,44.757049,-92.866069,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Tailwater'}]",Mississippi River Lock and Dam 02 Tailwater,Mississippi River Lock and Dam 02 Tailwater,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Baldhill_Dam-TainterGate03,Jamestown,Baldhill Dam Tainter Gate 03,OUTLET,US/Central,47.0361844,-98.081466,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate03'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate03'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,Highway75_Dam-ServiceSpillway,Watertown,Highway 75 Dam Service Spillway,SITE,US/Central,45.226761111111,-96.290252777778,ft,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-ServiceSpillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ODEM5'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-ServiceSpillway'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_10-CP_Clayton,Dubuque,Lock and Dam 10 Control Point,SITE,US/Central,42.785,-91.095,ft,United States,IA,Clayton,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_10-CP_Clayton'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GTTI4-CP_Clayton'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_10-CP_Clayton'}, {'name': 'Agency Aliases-NIDID', 'value': 'IA04014-CP_Clayton'}]",,Mississippi River Lock and Dam 10,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate02,Hastings,Lock and Dam 02 Tainter Gate 02,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate02'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Turbine02,Hastings,Lock and Dam 02 Turbine 02,TURBINE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Turbine02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Turbine02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Turbine02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Turbine02'}]",,Mississippi River Lock and Dam 02 Turbine 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-CrookedSlough,Winona,Crooked Slough near Lock5a,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-CrookedSlough'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-CrookedSlough'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-CrookedSlough'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-CrookedSlough'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-Bays07_08,Brainerd,Pine River Dam Bays 7 & 8 - Slide Gates,SITE,US/Central,46.669117,-94.11283,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays07_08'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays07_08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays07_08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays07_08'}]",Pine River Dam Bays 7 & 8 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 7, 8" +MVP,MissHW_PineRiver-SlideGate09,Brainerd,Pine River Dam Slide Gate 09,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate09'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05a-RollerGate03,Winona,Lock and Dam 05a Roller Gate 03,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate03'}]",Mississippi River Lock and Dam 05a Roller Gate 03,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-Bays03_04,Brainerd,Pine River Dam Bays 3 & 4 - Slide Gates,SITE,US/Central,46.669168,-94.112654,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays03_04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays03_04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays03_04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays03_04'}]",Pine River Dam Bays 3 & 4 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 3, 4" +MVP,LockDam_05a-RollerGate05,Winona,Lock and Dam 05a Roller Gate 05,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate05'}]",Mississippi River Lock and Dam 05a Roller Gate 05,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Lock_01,Hastings,Lock and Dam Lock 01,LOCK,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Lock_01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Lock_01'}]",,Mississippi River Lock and Dam 02,Lock,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-SlideGate07,Brainerd,Pine River Dam Slide Gate 07,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate07'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate07'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,MissHW_Gull-Fishway,Brainerd,Gull Fishway 01,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Fishway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Fishway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Fishway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Fishway'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_02-TainterGate04,Hastings,Lock and Dam 02 Tainter Gate 04,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate04'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-Lock_01,Winona,Lock and Dam 05 Lock 01,LOCK,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-Lock_01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-Lock_01'}]",,Mississippi River Lock and Dam 05,Lock,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate06,Hastings,Lock and Dam 02 Tainter Gate 06,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate06'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a,Fountain City,Lock and Dam 5A,PROJECT,US/Central,44.0890583,-91.6722333,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588'}]",Lock and Dam 05a at Mississippi River 9 foot Channel Navigation Project,Lock and Dam 05a,Dam,0.0,0.0,NAD83,599.9999999999999,LOCAL,USACE Owned and Maintained +MVP,LockDam_02-CP_SoStPaul,Hastings,Lock and Dam 02 Control Point,SITE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-CP_SoStPaul'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-CP_SoStPaul'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-CP_SoStPaul'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-CP_SoStPaul'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate05,Brainerd,Pine River Dam Slide Gate 05,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate05'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05-RollerGate02,Winona,Lock and Dam 05 Roller Gate 02,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate02'}]",Mississippi River Lock and Dam 05 Roller Gate 02,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_Gull,Brainerd,Gull Lake Dam,PROJECT,US/Central,46.4110944,-94.353575,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull'}]",Mississippi River Headwater Gull Lake Dam,Gull Lake Dam,Dam,,,NAD83,1099.9999999999998,NGVD29,USACE Owned and Maintained +MVP,LockDam_05-TainterValves,Winona,Lock and Dam 05 Tainter Valves,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterValves'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterValves'}]",Mississippi River Lock and Dam 05 Tainter Valves,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate04,Winona,Lock and Dam 05 Roller Gate 04,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate04'}]",Mississippi River Lock and Dam 05 Roller Gate 04,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Rafferty_Dam,Williston,Rafferty Reservoir,SITE,US/Central,49.1455556,-103.0944444,m,Canada,00,Unknown County or County N/A for Unknown State or State N/A,,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Rafferty_Dam'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'RAFQ8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Rafferty_Dam'}, {'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB032'}]","Rafferty Reservoir near Estevan, SK",Rafferty Reservoir near Estevan,DCP Gage,,,,,,Owner is Environment Canada +MVP,LockDam_02-TainterGate08,Hastings,Lock and Dam 02 Tainter Gate 08,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate08'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate08'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate13,Brainerd,Pine River Dam Slide Gate 13,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate13'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,MissHW_PineRiver-Bays11_13,Brainerd,"Pine River Dam Bays 11, 12, & 13 - Slide Gates",SITE,US/Central,46.669057,-94.113025,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays11_13'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays11_13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays11_13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays11_13'}]","Pine River Dam Bays 11, 12, & 13 - Slide Gates",Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 11, 12, 13" +MVP,LockDam_02-TainterGate10,Hastings,Lock and Dam 02 Tainter Gate 10,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate10'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate10'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02,Hastings,Lock and Dam 2,PROJECT,US/Central,44.7621,-92.8712833,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5'}]",Lock and Dam 02 at Mississippi River 9 foot ChannelNavigation Project,Lock and Dam 02,Dam,0.0,0.0,NAD83,599.9999999999999,LOCAL,"USACE Owned and Maintained, Brookfield Power operates hydropower" +MVP,LockDam_05a-TainterGates,Winona,Lock and Dam 05a Tainter Gates,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGates'}]",Mississippi River Lock and Dam 05a Tainter Gates,Mississippi River Lock and Dam 05a Tainter Gates,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-Tailwater,Brainerd,Pine River Dam Tailwater,SITE,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Tailwater'}]",,Pine River Dam at Cross Lake,Dam,,,NAD83,365.76,NGVD29, +MVP,MissHW_PineRiver-SlideGate11,Brainerd,Pine River Dam Slide Gate 11,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate11'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate11'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_05-RollerGate06,Winona,Lock and Dam 05 Roller Gate 06,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate06'}]",Mississippi River Lock and Dam 05 Roller Gate 06,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, diff --git a/load_data/data/.ipynb_checkpoints/MVP_timeseries_ids-checkpoint.csv b/load_data/data/.ipynb_checkpoints/MVP_timeseries_ids-checkpoint.csv new file mode 100755 index 0000000000..8c5cea6658 --- /dev/null +++ b/load_data/data/.ipynb_checkpoints/MVP_timeseries_ids-checkpoint.csv @@ -0,0 +1,1653 @@ +office,ts_id +mvp,LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-MainLake.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,TraverseWR_Dam-MainLake.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LeafGate.Elev.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LeafGate.Elev.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LeafGate.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LeafGate.Head.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam-LowFlow.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LowFlow.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Highway75_Dam-LowFlow.Precip-inc.Total.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-LowFlow.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam-LowFlow.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-ServiceSpillway.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Highway75_Dam.Area.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,Highway75_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-In.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Flow-Out.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,Highway75_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Head.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Stor.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-SlideGate01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate25.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate25.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate25.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05a-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05a-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_05-TainterGate27.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate27.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate27.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Flow.Inst.15Minutes.0.comp +mvp,ZUMM5.Flow.Inst.15Minutes.0.rev +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ZUMM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,ZUMM5.Stage.Inst.15Minutes.0.rev +mvp,ZUMM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ZUMM5.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver-SlideGate04.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LFKM5S.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,LFKM5S.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate02.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate01.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate03.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate05.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate06.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate07.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate08.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate09.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate10.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate11.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate12.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate13.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_PineRiver.Area.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Month.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Week.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.3Days.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Flow-In.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Flow-Out.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Flow-Out.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Flow.Inst.15Minutes.0.comp-gates +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.rev +mvp,MissHW_PineRiver.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,MissHW_PineRiver.Precip-inc.Total.1Day.1Day.comp +mvp,MissHW_PineRiver.Precip-inc.Total.1Hour.1Hour.comp +mvp,MissHW_PineRiver.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver.Stage.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_PineRiver.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Stage.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Stage.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Stor.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate13.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Lake.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Lake.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Lake.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.rev +mvp,MissHW_Gull-Lake.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Lake.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Lake.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.merged-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.rev +mvp,MissHW_Gull-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05-TainterGate15.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate17.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate19.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,DAWM5.Flow.Inst.15Minutes.0.Raw-MDNR +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,DAWM5.Stage.Inst.15Minutes.0.Raw-MDNR +mvp,MissHW_Gull-SlideGate05.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate21.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate21.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate21.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate13.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate32.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate32.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate32.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Flow.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate33.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate33.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate33.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-Tailwater.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:48+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:17+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:27+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:47+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:10+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.rev-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Temp-Water.Ave.1Day.1Day.merged +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.merged +mvp,Baldhill_Dam-Tailwater.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate17.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate34.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate34.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate34.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate29.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate29.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate29.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate30.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate30.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate30.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Clayton.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Clayton.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Clayton.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Stage.Inst.15Minutes.0.rev +mvp,Clayton.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Clayton.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Clayton.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate31.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate31.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate31.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate11.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate12.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate14.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate16.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate18.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate20.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate20.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate20.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate22.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate22.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate22.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate24.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate24.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate24.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate26.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate26.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate26.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate28.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate28.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate28.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_05.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05.Elev.Inst.12Hours.0.740Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.741Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.742Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.743Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.745Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.746Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.747Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.748Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.749Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_05.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_05.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_05.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_05.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_05.Flow.Ave.1Day.1Day.merged +mvp,LockDam_05.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05.Flow.Inst.15Minutes.0.merged +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Head.Inst.15Minutes.0.comp +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Temp-Water.Ave.1Day.1Day.merged +mvp,LockDam_05.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Temp-Water.Inst.15Minutes.0.merged +mvp,LockDam_05.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM +mvp,LockDam_05.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a-CrookedSlough.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.rev +mvp,LockDam_05a-CrookedSlough.Temp-Water.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-RollerGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a-Spillway.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05a-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05a.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_05a.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05a.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05a.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05a.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05a.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05a.Elev.Inst.12Hours.0.730Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.12Hours.0.731Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05a.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05a.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05a.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_05a.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_05a.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_05a.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_05a.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05a.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_05a.Flow.Ave.1Day.1Day.merged +mvp,LockDam_05a.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a.Flow.Inst.15Minutes.0.merged +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Head.Inst.15Minutes.0.comp +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05a.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05a.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05a.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,LockDam_05a.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate05.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,Baldhill_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.%-Ice.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Area.Inst.15Minutes.0.comp +mvp,Baldhill_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,Baldhill_Dam.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.1Day.0.Regulating +mvp,Baldhill_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:46+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:16+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:26+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:45+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:08+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~1Day.0.Regulating +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.rev +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.comp +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.rev +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:45+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:15+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:25+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:44+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:08+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-In.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:45+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:15+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:25+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:44+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:07+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.merged +mvp,Baldhill_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.merged +mvp,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.1Hour.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:47+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:16+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:27+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:46+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:09+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow.Inst.15Minutes.0.comp-gates +mvp,Baldhill_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:37+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:09+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:18+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:37+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:01+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:04+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:30+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:42+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:01+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:24+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Baldhill_Dam.Precip-inc.Total.1Day.1Day.comp +mvp,Baldhill_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam.Stage.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam.Stor.Ave.15Minutes.2Hours.comp +mvp,Baldhill_Dam.Stor.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,Baldhill_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Powerhouse.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Powerhouse.Power.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Powerhouse.Power.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Rafferty_Dam-Tailwater.Flow.Inst.5Minutes.0.Raw-EnvCan +mvp,Rafferty_Dam-Tailwater.Stage.Inst.5Minutes.0.Raw-EnvCan +mvp,Cooperstown200.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Cooperstown200.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Cooperstown200.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown200.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-TainterGate.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-TainterGate.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ELZM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,ELZM5.Flow.Inst.0.0.Raw-USGS +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ELZM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ELZM5.Stage.Inst.0.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Lake.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Lake.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Lake.Stor.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Lake.Stor.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate01.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam.Area.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.rev +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.comp +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.rev +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam.Flow-Out.Inst.1Hour.0.rev +mvp,TraverseWR_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry +mvp,TraverseWR_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam.Stor.Ave.15Minutes.2Hours.comp +mvp,TraverseWR_Dam.Stor.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Cooperstown.Conc-OXYGEN.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Cooperstown.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Cooperstown.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Cooperstown.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:42+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:13+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:23+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:42+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:05+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Flow.Ave.1Day.1Day.rev-USGS +mvp,Cooperstown.Flow.Inst.15Minutes.0.comp +mvp,Cooperstown.Flow.Inst.15Minutes.0.rev +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:43+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:13+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:23+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:42+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:06+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:07+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:32+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:45+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:04+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:27+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:07+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:31+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:44+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:04+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:26+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Stage.Ave.1Day.1Day.rev-USGS +mvp,Cooperstown.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Stage.Inst.15Minutes.0.corrected-comp +mvp,Cooperstown.Stage.Inst.15Minutes.0.rev +mvp,Cooperstown.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Cooperstown.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MOOM5S.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,MOOM5S.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam.Area.Inst.15Minutes.0.comp +mvp,ChippewaDiv_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM +mvp,ChippewaDiv_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM +mvp,ChippewaDiv_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.merged +mvp,ChippewaDiv_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.merged +mvp,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,ChippewaDiv_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam.Stor.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Muscoda.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Muscoda.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Muscoda.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Flow.Ave.1Day.1Day.rev-USGS +mvp,Muscoda.Flow.Inst.15Minutes.0.comp +mvp,Muscoda.Flow.Inst.15Minutes.0.rev +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.rev +mvp,Muscoda.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Muscoda.Precip-inc.Total.1Day.1Day.comp +mvp,Muscoda.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Stage.Inst.15Minutes.0.corrected-comp +mvp,Muscoda.Stage.Inst.15Minutes.0.rev +mvp,Muscoda.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-SlideGate04.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-StopLog01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,NWUM5.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,NWUM5.Elev.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Elev.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,NWUM5.Flow.Inst.0.0.Raw-USGS +mvp,NWUM5.Flow.Inst.15Minutes.0.comp +mvp,NWUM5.Flow.Inst.15Minutes.0.rev +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Stage.Inst.0.0.Raw-USGS +mvp,NWUM5.Stage.Inst.15Minutes.0.OTHER-GOES-Raw +mvp,NWUM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,NWUM5.Stage.Inst.15Minutes.0.rev +mvp,NWUM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Volt.Inst.1Hour.0.OTHER-GOES-Raw +mvp,LockDam_02-TainterGate12.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate12.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,AGYM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,AGYM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,AGYM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_02-TainterGate16.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LFKM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,LFKM5.Flow.Inst.15Minutes.0.comp +mvp,LFKM5.Flow.Inst.15Minutes.0.rev +mvp,LFKM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,LFKM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,LFKM5.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Precip-cum.Inst.15Minutes.0.rev +mvp,LFKM5.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,LFKM5.Precip-inc.Total.1Day.1Day.comp +mvp,LFKM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,LFKM5.Stage.Inst.15Minutes.0.rev +mvp,LFKM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,LFKM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LFKM5.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Flow.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Stage.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_02-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02-Tailwater.Flow.Ave.1Day.1Day.merged +mvp,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.merged +mvp,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_02-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_02-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Fishway.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Fishway.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_Gull.Area.Inst.1Hour.0.comp +mvp,MissHW_Gull.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_Gull.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_Gull.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_Gull.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Elev.Inst.1Hour.0.merged-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Month.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Week.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.3Days.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,MissHW_Gull.Flow-In.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Flow-In.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull.Flow-Out.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Flow-Out.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.rev +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Flow.Inst.1Hour.0.comp-gates +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.rev +mvp,MissHW_Gull.Precip-inc.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-inc.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-inc.Total.1Day.1Day.comp +mvp,MissHW_Gull.Precip-inc.Total.1Hour.1Hour.comp +mvp,MissHW_Gull.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.rev +mvp,MissHW_Gull.Stage.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,MissHW_Gull.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Rafferty_Dam.Elev.Inst.5Minutes.0.Raw-EnvCan +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Stage.Inst.5Minutes.0.Raw-EnvCan +mvp,LockDam_02-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate08.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_02.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_02.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_02.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_02.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_02.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_02.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_02.Elev.Inst.12Hours.0.818Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.819Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.820Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.823Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.824Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.827Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.837Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.839Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_02.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_02.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_02.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_02.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_02.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_02.Flow.Ave.1Day.1Day.merged +mvp,LockDam_02.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02.Flow.Inst.15Minutes.0.merged +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Head.Inst.15Minutes.0.comp +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02.Stage.Ave.1Day.1Day.comp +mvp,LockDam_02.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_02.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Water.Ave.1Day.1Day.merged +mvp,LockDam_02.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Temp-Water.Inst.15Minutes.0.merged +mvp,LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM +mvp,LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw diff --git a/load_data/data/LRL_locations_data.csv b/load_data/data/LRL_locations_data.csv new file mode 100755 index 0000000000..b8219d8e3f --- /dev/null +++ b/load_data/data/LRL_locations_data.csv @@ -0,0 +1,53 @@ +office,name,kind,unit,nation,active,aliases,nearest-city,time-zone,latitude,longitude,horizontal-datum,elevation,vertical-datum,state,county,bounding-office,map-label,public-name,long-name,type,published-latitude,published-longitude,description +LRL,Taylorsville-Lake,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295597'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Lake'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Lake'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,466.84999999999997,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Green-GRLK2,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03007-GRLK2'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,203.6064,NGVD29,KY,Taylor,LRL,Green River Lake,,,,,, +LRL,Boston-BSNK2L,SITE,m,United States,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE693326-BSNK2L'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03301500-BSNK2L'}]",,US/Eastern,37.767286111111,-85.70385,,122.048016,NGVD29,KY,Nelson,,,,,,,, +LRL,Green,PROJECT,ft,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03007'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,668.0,NGVD29,KY,Taylor,LRL,Green River Lake,Green River Lake,Green,Corps Reservoir,,, +LRL,Taylorsville-Bypass02,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Bypass02'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Bypass02'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Catawba,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03253500'}]","Falmouth, KY",US/Eastern,38.7103478,-84.3107693,NAD27,500.01,NGVD29,KY,Pendleton,LRL,Licking River at Catawba,"LICKING RIVER AT CATAWBA, KY","LICKING RIVER AT CATAWBA, KY",Stream Gauge,,, +LRL,Buckhorn-Lake,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03280800'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY03027-Lake'}]","Buckhorn, KY",US/Eastern,37.3402778,-83.4691667,NAD27,757.0,NGVD29,KY,Perry,LRL,Buckhorn Lake,,,,,, +LRL,TaylorsvilleOH,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03263000'}]",,America/New_York,,,,,,,,,,,,,,, +LRL,DUNK2S,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,BKVI3,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Cincinnati-CCNO1,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03255000-CCNO1'}]","Covington, KY",US/Eastern,39.0945044,-84.5104983,NAD27,428.87999999999994,NGVD29,OH,Hamilton,LRL,Ohio River at Cincinnati,,,,,, +LRL,Olmsted-HW,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-HW'}]",Presque Isle,Unknown or Not Applicable,0.0,0.0,,0.0,,00,Unknown County or County N/A for Unknown State or State N/A,,,Olmsted Headwater,,,-3.4028234663852886e+38,-3.4028234663852886e+38, +LRL,Boston,SITE,ft,,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE693326'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03301500'}]",,US/Eastern,37.767286111111,-85.70385,,400.41999999999996,NGVD29,KY,Nelson,,,"ROLLING FORK NEAR BOSTON, KY","ROLLING FORK NEAR BOSTON, KY",,,, +LRL,ALVK2,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-Outlet01,OUTLET,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Outlet01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Outlet01'}]","Taylorsville, KY",US/Eastern,38.005360231786,-85.307965492868,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,Taylorsville Outlet,,,,, +LRL,Taylorsville-BRCK2,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-BRCK2'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-BRCK2'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Green-Lake,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03305990'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY03007-Lake'}]","Cane Valley, KY",US/Eastern,37.2461111,-85.3391667,NAD27,203.6064,NGVD29,KY,Taylor,LRL,Green River Lake,,,,,, +LRL,Greensburg,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03306500'}]","Greensburg, KY",US/Central,37.2536713,-85.5030215,NAD83,531.81,NGVD29,KY,Green,LRL,,"GREEN RIVER AT GREENSBURG, KY","GREEN RIVER AT GREENSBURG, KY",,,, +LRL,Cairo,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '370000089094501'}]",,,,,,,,,,,,,,,,, +LRL,BowlingGreenKY-BWGK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE774640-BWGK2L'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03314500-BWGK2L'}]",,US/Central,37.002819444444,-86.432766666667,,409.83,NGVD29,KY,Warren,,,,,,,, +LRL,Franklin-ALVK2S,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03313700-ALVK2S'}]","Franklin, KY",US/Central,36.7233738,-86.5522175,NAD83,174.799752,NGVD29,KY,Simpson,LRL,,,,,,, +LRL,Newburgh,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03304300'}]","Newburgh, IN",US/Central,37.934301333332,-87.379384665112,NAD83,100.434648,NGVD29,IN,Warrick,LRL,Newburgh Locks and Dam TW,OHIO RIVER AT NEWBURGH LOCK AND ,"OHIO RIVER AT NEWBURGH LOCK AND DAM, IN",Stream Gauge,,, +LRL,Alvaton-ALVK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03314000-ALVK2L'}, {'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE6D10F0-ALVK2L'}]",,US/Central,36.895319444444,-86.380547222222,,443.0699999999999,NGVD29,KY,Warren,,,,,,,, +LRL,Taylorsville-Tailwater,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Tailwater'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Barkley-BARK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,GRLK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Buckhorn,PROJECT,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY03027'}]","Buckhorn, KY",US/Eastern,37.3402778,-83.4691667,NAD27,230.73360000000002,NGVD29,KY,Perry,LRL,Buckhorn Lake,Buckhorn Lake,Buckhorn Lake,Corps Reservoir,,, +LRL,Kentucky-KYDK2,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-ServiceGate01,SITE,m,United States,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'KY00051-ServiceGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-ServiceGate01'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Taylorsville-ServiceGate02,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-ServiceGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-ServiceGate02'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Taylorsville-Bypass01,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-Bypass01'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-Bypass01'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,Cairo-CIRI2,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '370000089094501-CIRI2'}]",,,,,,,,,,,,,,,,, +LRL,Olmsted,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600'}]",Presque Isle,America/Chicago,0.0,0.0,,0.0,,,,,,,,,0.0,0.0, +LRL,Taylorsville,PROJECT,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,Taylorsville Lake,Taylorsville Lake,Corps Reservoir,,, +LRL,Dundee-DUNK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03319000-DUNK2L'}]","Narrows, KY",US/Central,37.5475514,-86.7216546,NAD83,393.18,NGVD29,KY,Ohio,LRL,,,,,,, +LRL,HorseBranch-DUNK2S,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03318800-DUNK2S'}]",,,,,,,,,,,,,,,,, +LRL,BowlingGreenKY,SITE,ft,,True,"[{'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE774640'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '03314500'}]",,US/Central,37.002819444444,-86.432766666667,,409.83,NGVD29,KY,Warren,,,"BARREN RIVER AT BOWLING GREEN, K","BARREN RIVER AT BOWLING GREEN, KY",,,, +LRL,DUNK2,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Olmsted-TW,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-TW'}]",Presque Isle,Unknown or Not Applicable,0.0,0.0,,0.0,,00,Unknown County or County N/A for Unknown State or State N/A,,,Olmsted Tailwater,,,-3.4028234663852886e+38,-3.4028234663852886e+38, +LRL,ALVK2S,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Taylorsville-BRCK2L,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-BRCK2L'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-BRCK2L'}]","Taylorsville, KY",US/Eastern,38.003568,-85.3065171,NAD27,142.29588,NGVD29,KY,Spencer,LRL,Taylorsville Lake,,,,,, +LRL,OlmstedUpr,SITE,m,United States,True,[],"Olmsted, IL",US/Central,37.183424262929,-89.063253433899,NAD83,84.86851200000001,NGVD29,KY,Ballard,LRL,Olmsted Dam Upper Gage,OHIO RIVER AT OLMSTED,OHIO RIVER AT OLMSTED,Stream Gauge,,, +LRL,Taylorsville-TAYO1,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03295890-TAYO1'}, {'name': 'Agency Aliases-NIDID', 'value': 'KY00051-TAYO1'}]","Vandalia, OH",US/Eastern,39.8728351,-84.164107,NAD83,760.11,NGVD29,OH,Montgomery,LRL,Taylorsville Dam,Great Miami River at Taylorsvill,Great Miami River at Taylorsville OH,Stream Gauge,,, +LRL,BowlingGreenIN,SITE,m,,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03360000'}, {'name': 'Agency Aliases-DCP Platform ID', 'value': 'CE77835E'}]",,US/Eastern,39.382819444444,-87.020566666667,,167.036496,NGVD29,IN,Clay,,,"EEL RIVER AT BOWLING GREEN, IN","EEL RIVER AT BOWLING GREEN, IN",Stream Gauge,,, +LRL,Newburghwq,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,GreenMouth,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Kentucky,SITE,m,United States,True,[],,,,,,,,,,,,,,,,, +LRL,Cincinnati,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03255000'}]","Covington, KY",US/Eastern,39.0945044,-84.5104983,NAD27,428.87999999999994,NGVD29,OH,Hamilton,LRL,Ohio River at Cincinnati,"OHIO RIVER AT CINCINNATI, OH","OHIO RIVER AT CINCINNATI, OH",Stream Gauge,,, +LRL,Newburgh-NBGI3,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03304300-NBGI3'}]","Newburgh, IN",Unknown or Not Applicable,37.934301333332,-87.379384665112,NAD83,100.434648,NGVD29,00,Unknown County or County N/A for Unknown State or State N/A,LRL,Newburgh Locks and Dam TW,,,,,, +LRL,Olmsted-OLMI2,SITE,m,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03612600-OLMI2'}]",Presque Isle,America/Chicago,0.0,0.0,,0.0,,,,,,,,,0.0,0.0, +LRL,Greensburg-GNSK2L,SITE,ft,United States,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '03306500-GNSK2L'}]","Greensburg, KY",US/Central,37.2536713,-85.5030215,NAD83,531.81,NGVD29,KY,Green,LRL,,,,,,, +LRL,Barkley,SITE,ft,United States,True,[],,,,,,,,,,,,,,,,, diff --git a/load_data/data/LRL_timeseries_ids_all.csv b/load_data/data/LRL_timeseries_ids_all.csv new file mode 100755 index 0000000000..eaadbba2a6 --- /dev/null +++ b/load_data/data/LRL_timeseries_ids_all.csv @@ -0,0 +1,444 @@ +,office,ts_id +0,LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-raw +1,LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-rev +2,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-raw +3,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-rev +4,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-raw +5,LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-rev +6,LRL,Taylorsville-Lake.Precip.Inst.15Minutes.0.LRGS-raw +7,LRL,Taylorsville-Lake.Precip.Inst.5Minutes.0.LRGS-raw +8,LRL,Taylorsville-Lake.Precip.Total.5Minutes.5Minutes.USGS-raw +9,LRL,Taylorsville-Lake.Precip.Total.5Minutes.5Minutes.USGS-rev +10,LRL,Taylorsville-Lake.Temp-Water.Inst.15Minutes.0.USGS-raw +11,LRL,Taylorsville-Lake.Temp-Water.Inst.15Minutes.0.USGS-rev +12,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.LRGS-comp +13,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.USGS-raw +14,LRL,BowlingGreenIN.Flow.Inst.15Minutes.0.USGS-rev +15,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.LRGS-raw +16,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.LRGS-rev +17,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.USGS-raw +18,LRL,BowlingGreenIN.Stage.Inst.15Minutes.0.USGS-rev +19,LRL,BowlingGreenIN.Stage.Inst.~1Day.0.lrldlb-raw +20,LRL,BowlingGreenIN.Stage.Inst.~1Day.0.lrldlb-rev +21,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.LRGS-comp +22,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-raw +23,LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-rev +24,LRL,BowlingGreenKY.Precip.Inst.15Minutes.0.LRGS-raw +25,LRL,BowlingGreenKY.Precip.Total.15Minutes.15Minutes.USGS-raw +26,LRL,BowlingGreenKY.Precip.Total.15Minutes.15Minutes.USGS-rev +27,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-raw +28,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-rev +29,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-raw +30,LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-rev +31,LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-raw +32,LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-rev +33,LRL,GREENUP-GNUK2.Code-Ice.Inst.0.0.LPMS-raw +34,LRL,GREENUP-GNUK2.Code-Weather.Inst.0.0.LPMS-raw +35,LRL,GREENUP-GNUK2.Count-Hydropower-Units.Inst.0.0.LPMS-raw +36,LRL,GREENUP-GNUK2.Dir-Wind.Inst.0.0.LPMS-raw +37,LRL,GREENUP-GNUK2.Flow-Hydropower.Inst.0.0.LPMS-raw +38,LRL,GREENUP-GNUK2.Flow-Total.Inst.0.0.LPMS-comp +39,LRL,GREENUP-GNUK2.Opening-Gate-Total.Inst.0.0.LPMS-raw +40,LRL,GREENUP-GNUK2.Precip.Total.0.1Day.LPMS-raw +41,LRL,GREENUP-GNUK2.Speed-Wind.Inst.0.0.LPMS-raw +42,LRL,GREENUP-GNUK2.Stage-Headwater.Inst.0.0.LPMS-raw +43,LRL,GREENUP-GNUK2.Stage-Headwater.Inst.1Day.0.LPMS-comp +44,LRL,GREENUP-GNUK2.Stage-Tailwater.Inst.0.0.LPMS-raw +45,LRL,GREENUP-GNUK2.Stage-Tailwater.Inst.1Day.0.LPMS-comp +46,LRL,GREENUP-GNUK2.Temp-Air.Inst.0.0.LPMS-raw +47,LRL,GREENUP-GNUK2.Temp-Water.Inst.0.0.LPMS-raw +48,LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-raw +49,LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-rev +50,LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-raw +51,LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-rev +52,LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-raw +53,LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-rev +54,LRL,Green-Lake.Precip.Inst.15Minutes.0.LRGS-raw +55,LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-raw +56,LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-rev +57,LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-raw +58,LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-rev +59,LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-raw +60,LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-rev +61,LRL,Green.Depth-Snow.Inst.~1Day.0.radar-test +62,LRL,Green.Depth-SnowWE.Inst.~1Day.0.radar-test +63,LRL,Green.Elev.Inst.0.0.lrldlb-raw +64,LRL,Green.Elev.Inst.0.0.lrldlb-rev +65,LRL,Green.Elev.Inst.0.0.radar-test +66,LRL,Green.Elev.Inst.15Minutes.0.LRGS-raw +67,LRL,Green.Elev.Inst.15Minutes.0.LRGS-rev +68,LRL,Green.Elev.Inst.1Hour.0.LRL-cavi-fct +69,LRL,Green.Elev.Inst.1Hour.0.National-CWMS-Forecast +70,LRL,Green.Elev.Inst.1Hour.0.lrldlb-comp +71,LRL,Green.Elev.Inst.~1Day.0.LRL-cavi-fct +72,LRL,Green.Flow-BP1.Inst.0.0.lrldlb-comp +73,LRL,Green.Flow-BP1.Inst.1Hour.0.lrldlb-comp +74,LRL,Green.Flow-BP2.Inst.0.0.lrldlb-comp +75,LRL,Green.Flow-BP2.Inst.1Hour.0.lrldlb-comp +76,LRL,Green.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +77,LRL,Green.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +78,LRL,Green.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +79,LRL,Green.Flow-MG1.Inst.0.0.lrldlb-comp +80,LRL,Green.Flow-MG1.Inst.1Hour.0.lrldlb-comp +81,LRL,Green.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +82,LRL,Green.Flow-Outflow.Inst.0.0.lrldlb-comp +83,LRL,Green.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +84,LRL,Green.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +85,LRL,Green.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +86,LRL,Green.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +87,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-fct +88,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-raw +89,LRL,Green.Opening-BP1.Inst.0.0.lrldlb-rev +90,LRL,Green.Opening-BP1.Inst.0.0.radar-test +91,LRL,Green.Opening-BP1.Inst.1Hour.0.lrldlb-comp +92,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-fct +93,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-raw +94,LRL,Green.Opening-BP2.Inst.0.0.lrldlb-rev +95,LRL,Green.Opening-BP2.Inst.0.0.radar-test +96,LRL,Green.Opening-BP2.Inst.1Hour.0.lrldlb-comp +97,LRL,Green.Opening-L1.Inst.0.0.lrldlb-fct +98,LRL,Green.Opening-L1.Inst.0.0.lrldlb-raw +99,LRL,Green.Opening-L1.Inst.0.0.lrldlb-rev +100,LRL,Green.Opening-L1.Inst.0.0.radar-test +101,LRL,Green.Opening-L1.Inst.1Hour.0.lrldlb-comp +102,LRL,Green.Opening-L2.Inst.0.0.lrldlb-fct +103,LRL,Green.Opening-L2.Inst.0.0.lrldlb-raw +104,LRL,Green.Opening-L2.Inst.0.0.lrldlb-rev +105,LRL,Green.Opening-L2.Inst.0.0.radar-test +106,LRL,Green.Opening-L2.Inst.1Hour.0.lrldlb-comp +107,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-fct +108,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-raw +109,LRL,Green.Opening-MG1.Inst.0.0.lrldlb-rev +110,LRL,Green.Opening-MG1.Inst.0.0.radar-test +111,LRL,Green.Opening-MG1.Inst.1Hour.0.lrldlb-comp +112,LRL,Green.Opening-MG2.Inst.0.0.lrldlb-raw +113,LRL,Green.Opening-MG2.Inst.0.0.lrldlb-rev +114,LRL,Green.Opening-MG2.Inst.1Hour.0.lrldlb-comp +115,LRL,Green.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +116,LRL,Green.Precip.Inst.15Minutes.0.LRGS-raw +117,LRL,Green.Precip.Inst.1Hour.0.National-CWMS-Forecast +118,LRL,Green.Precip.Total.1Hour.1Hour.Openweather +119,LRL,Green.Precip.Total.~1Day.1Day.lrldlb-raw +120,LRL,Green.Precip.Total.~1Day.1Day.lrldlb-rev +121,LRL,Green.Precip.Total.~1Day.1Day.radar-test +122,LRL,Green.Stage-Tailwater.Inst.0.0.lrldlb-raw +123,LRL,Green.Stage-Tailwater.Inst.0.0.radar-test +124,LRL,Green.Stage.Inst.15Minutes.0.LRGS-raw +125,LRL,Green.Stage.Inst.15Minutes.0.LRGS-rev +126,LRL,Green.Stor.Inst.1Hour.0.lrldlb-comp +127,LRL,Green.Temp-Air.Inst.1Hour.0.Openweather +128,LRL,Green.Temp-Air.Inst.~1Day.0.lrldlb-raw +129,LRL,Green.Temp-Air.Inst.~1Day.0.radar-test +130,LRL,Green.Temp-Air.Max.~1Day.1Day.lrldlb-raw +131,LRL,Green.Temp-Air.Max.~1Day.1Day.radar-test +132,LRL,Green.Temp-Air.Min.~1Day.1Day.lrldlb-raw +133,LRL,Green.Temp-Air.Min.~1Day.1Day.radar-test +134,LRL,Green.Temp-Water.Inst.~1Day.0.lrldlb-raw +135,LRL,Green.Temp-Water.Inst.~1Day.0.radar-test +136,LRL,Greensburg.Flow.Inst.15Minutes.0.LRGS-comp +137,LRL,Greensburg.Precip.Inst.15Minutes.0.LRGS-raw +138,LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-raw +139,LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-rev +140,LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-raw +141,LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-rev +142,LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-raw +143,LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-rev +144,LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-raw +145,LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-rev +146,LRL,Greensburg.Stage.Inst.~1Day.0.radar-test +147,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.LRGS-raw +148,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.LRGS-rev +149,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.USGS-raw +150,LRL,Greensburg.Temp-Water.Inst.15Minutes.0.USGS-rev +151,LRL,Catawba.Flow.Inst.15Minutes.0.LRGS-comp +152,LRL,Catawba.Flow.Inst.15Minutes.0.USGS-raw +153,LRL,Catawba.Flow.Inst.15Minutes.0.USGS-rev +154,LRL,Catawba.Flow.Inst.~1Day.0.LRL-cavi-fct +155,LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-raw +156,LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-rev +157,LRL,Catawba.Stage.Inst.15Minutes.0.USGS-raw +158,LRL,Catawba.Stage.Inst.15Minutes.0.USGS-rev +159,LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-raw +160,LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-rev +161,LRL,Buckhorn-Lake.Elev.Inst.15Minutes.0.LRGS-raw +162,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-raw +163,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-rev +164,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-raw +165,LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-rev +166,LRL,Buckhorn-Lake.Precip.Inst.15Minutes.0.LRGS-raw +167,LRL,Buckhorn-Lake.Precip.Inst.5Minutes.0.LRGS-raw +168,LRL,Buckhorn-Lake.Precip.Total.5Minutes.5Minutes.USGS-raw +169,LRL,Buckhorn-Lake.Precip.Total.5Minutes.5Minutes.USGS-rev +170,LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-raw +171,LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-rev +172,LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-raw +173,LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-rev +174,LRL,TaylorsvilleOH.Elev.Inst.15Minutes.0.USGS-comp +175,LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-raw +176,LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-rev +177,LRL,TaylorsvilleOH.Precip.Total.15Minutes.15Minutes.USGS-raw +178,LRL,TaylorsvilleOH.Precip.Total.15Minutes.15Minutes.USGS-rev +179,LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-raw +180,LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-rev +181,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +182,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +183,LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +184,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +185,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +186,LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +187,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +188,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +189,LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +190,LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-raw +191,LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-rev +192,LRL,Boston.Flow.Inst.0.0.USGS-raw +193,LRL,Boston.Flow.Inst.0.0.USGS-rev +194,LRL,Boston.Flow.Inst.15Minutes.0.LRGS-comp +195,LRL,Boston.Flow.Inst.15Minutes.0.USGS-raw +196,LRL,Boston.Flow.Inst.15Minutes.0.USGS-rev +197,LRL,Boston.Flow.Inst.~1Day.0.LRL-cavi-fct +198,LRL,Boston.Precip.Inst.15Minutes.0.LRGS-raw +199,LRL,Boston.Precip.Total.15Minutes.15Minutes.USGS-raw +200,LRL,Boston.Precip.Total.15Minutes.15Minutes.USGS-rev +201,LRL,Boston.Stage.Inst.15Minutes.0.LRGS-raw +202,LRL,Boston.Stage.Inst.15Minutes.0.LRGS-rev +203,LRL,Boston.Stage.Inst.15Minutes.0.USGS-raw +204,LRL,Boston.Stage.Inst.15Minutes.0.USGS-rev +205,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +206,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +207,LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +208,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +209,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +210,LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-operQPF +211,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +212,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +213,LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +242,LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-0QPF +243,LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-48QPF +244,LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-0QPF +245,LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-48QPF +246,LRL,Cairo.Elev.Inst.1Hour.0.USGS-rev +247,LRL,Cairo.Precip.Inst.1Hour.0.LRGS-raw +248,LRL,Cairo.Stage.Inst.1Hour.0.LRGS-raw +249,LRL,Cairo.Stage.Inst.1Hour.0.LRGS-rev +250,LRL,Cairo.Stage.Inst.1Hour.0.USGS-raw +251,LRL,Cairo.Stage.Inst.1Hour.0.USGS-rev +252,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp +253,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp-SOModC +254,LRL,Newburgh-NBGI3.Flow-Total.Inst.0.0.LPMS-comp-USGS +255,LRL,Newburgh-NBGI3.Opening-Gate-Total.Inst.0.0.LPMS-raw +256,LRL,Newburgh-NBGI3.Stage-Headwater.Inst.0.0.LPMS-raw +257,LRL,Newburgh-NBGI3.Stage-Headwater.Inst.1Day.0.LPMS-comp +258,LRL,Newburgh-NBGI3.Stage-Tailwater.Inst.0.0.LPMS-raw +259,LRL,Newburgh-NBGI3.Stage-Tailwater.Inst.1Day.0.LPMS-comp +260,LRL,Newburgh.Precip.Inst.15Minutes.0.LRGS-raw +261,LRL,Newburgh.Precip.Total.15Minutes.15Minutes.USGS-raw +262,LRL,Newburgh.Precip.Total.15Minutes.15Minutes.USGS-rev +263,LRL,Newburgh.Stage.Inst.15Minutes.0.LRGS-raw +264,LRL,Newburgh.Stage.Inst.15Minutes.0.LRGS-rev +265,LRL,Newburgh.Stage.Inst.15Minutes.0.USGS-raw +266,LRL,Newburgh.Stage.Inst.15Minutes.0.USGS-rev +267,LRL,Barkley-BARK2.Flow.Ave.~6Hours.6Hours.TVA-fct +268,LRL,Barkley-BARK2.Flow.Inst.1Hour.0.TVA-obs +269,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +270,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +271,LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +272,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +273,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +274,LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-operQPF +288,LRL,Buckhorn.Depth-Snow.Inst.~1Day.0.radar-test +289,LRL,Buckhorn.Depth-SnowWE.Inst.~1Day.0.radar-test +290,LRL,Buckhorn.Elev.Inst.0.0.lrldlb-raw +291,LRL,Buckhorn.Elev.Inst.0.0.lrldlb-rev +292,LRL,Buckhorn.Elev.Inst.0.0.radar-test +293,LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-raw +294,LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-rev +295,LRL,Buckhorn.Elev.Inst.1Hour.0.LRL-cavi-fct +296,LRL,Buckhorn.Elev.Inst.1Hour.0.National-CWMS-Forecast +297,LRL,Buckhorn.Elev.Inst.1Hour.0.lrldlb-comp +298,LRL,Buckhorn.Elev.Inst.~1Day.0.LRL-cavi-fct +299,LRL,Buckhorn.Flow-BP1.Inst.0.0.lrldlb-comp +300,LRL,Buckhorn.Flow-BP1.Inst.1Hour.0.lrldlb-comp +301,LRL,Buckhorn.Flow-BP2.Inst.0.0.lrldlb-comp +302,LRL,Buckhorn.Flow-BP2.Inst.1Hour.0.lrldlb-comp +303,LRL,Buckhorn.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +304,LRL,Buckhorn.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +305,LRL,Buckhorn.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +306,LRL,Buckhorn.Flow-MG1.Inst.0.0.lrldlb-comp +307,LRL,Buckhorn.Flow-MG1.Inst.1Hour.0.lrldlb-comp +308,LRL,Buckhorn.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +309,LRL,Buckhorn.Flow-Outflow.Inst.0.0.lrldlb-comp +310,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +311,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +312,LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +313,LRL,Buckhorn.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +314,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-fct +315,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-raw +316,LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-rev +317,LRL,Buckhorn.Opening-BP1.Inst.0.0.radar-test +318,LRL,Buckhorn.Opening-BP1.Inst.1Hour.0.lrldlb-comp +319,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-fct +320,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-raw +321,LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-rev +322,LRL,Buckhorn.Opening-BP2.Inst.0.0.radar-test +323,LRL,Buckhorn.Opening-BP2.Inst.1Hour.0.lrldlb-comp +324,LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-raw +325,LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-rev +326,LRL,Buckhorn.Opening-L1.Inst.1Hour.0.lrldlb-comp +327,LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-raw +328,LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-rev +329,LRL,Buckhorn.Opening-L2.Inst.1Hour.0.lrldlb-comp +330,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-fct +331,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-raw +332,LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-rev +333,LRL,Buckhorn.Opening-MG1.Inst.0.0.radar-test +334,LRL,Buckhorn.Opening-MG1.Inst.1Hour.0.lrldlb-comp +335,LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-raw +336,LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-rev +337,LRL,Buckhorn.Opening-MG2.Inst.1Hour.0.lrldlb-comp +338,LRL,Buckhorn.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +339,LRL,Buckhorn.Precip.Inst.15Minutes.0.LRGS-raw +340,LRL,Buckhorn.Precip.Inst.1Hour.0.National-CWMS-Forecast +341,LRL,Buckhorn.Precip.Total.1Hour.1Hour.Openweather +342,LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-raw +343,LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-rev +344,LRL,Buckhorn.Precip.Total.~1Day.1Day.radar-test +345,LRL,Buckhorn.Stage-Tailwater.Inst.0.0.lrldlb-raw +346,LRL,Buckhorn.Stage-Tailwater.Inst.0.0.radar-test +347,LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-raw +348,LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-rev +349,LRL,Buckhorn.Stor.Inst.1Hour.0.lrldlb-comp +350,LRL,Buckhorn.Temp-Air.Inst.1Hour.0.Openweather +351,LRL,Buckhorn.Temp-Air.Inst.~1Day.0.lrldlb-raw +352,LRL,Buckhorn.Temp-Air.Inst.~1Day.0.radar-test +353,LRL,Buckhorn.Temp-Air.Max.~1Day.1Day.lrldlb-raw +354,LRL,Buckhorn.Temp-Air.Max.~1Day.1Day.radar-test +355,LRL,Buckhorn.Temp-Air.Min.~1Day.1Day.lrldlb-raw +356,LRL,Buckhorn.Temp-Air.Min.~1Day.1Day.radar-test +357,LRL,Buckhorn.Temp-Water.Inst.~1Day.0.lrldlb-raw +358,LRL,Buckhorn.Temp-Water.Inst.~1Day.0.radar-test +359,LRL,Kentucky-KYDK2.Flow.Ave.~6Hours.6Hours.TVA-fct +360,LRL,Kentucky-KYDK2.Flow.Inst.1Hour.0.TVA-obs +367,LRL,Olmsted-OLMI2.Code-Ice.Inst.0.0.LPMS-raw +368,LRL,Olmsted-OLMI2.Code-Weather.Inst.0.0.LPMS-raw +369,LRL,Olmsted-OLMI2.Count-Needles.Inst.0.0.LPMS-raw +370,LRL,Olmsted-OLMI2.Count-Wickets-Down.Inst.0.0.LPMS-raw +371,LRL,Olmsted-OLMI2.Dir-Wind.Inst.0.0.LPMS-raw +372,LRL,Olmsted-OLMI2.Elev-Headwater.Inst.15Minutes.0.USGS-comp +373,LRL,Olmsted-OLMI2.Elev-Tailwater.Inst.15Minutes.0.USGS-comp +374,LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-raw +375,LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-rev +376,LRL,Olmsted-OLMI2.Opening-Gate-Average.Inst.1Hour.0.LPMS-comp +377,LRL,Olmsted-OLMI2.Opening-Gate-Total.Inst.0.0.LPMS-raw +378,LRL,Olmsted-OLMI2.Precip.Total.0.1Day.LPMS-raw +379,LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-raw +380,LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-rev +381,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.0.0.LPMS-raw +382,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-raw +383,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-rev +384,LRL,Olmsted-OLMI2.Stage-Headwater.Inst.1Day.0.LPMS-comp +385,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.0.0.LPMS-raw +386,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-raw +387,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-rev +388,LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.1Day.0.LPMS-comp +389,LRL,Olmsted-OLMI2.Temp-Air.Inst.0.0.LPMS-raw +390,LRL,Olmsted-OLMI2.Temp-Water.Inst.0.0.LPMS-raw +391,LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-raw +392,LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-rev +393,LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-raw +394,LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-rev +395,LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-raw +396,LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-rev +397,LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-raw +398,LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-rev +399,LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-raw +400,LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-rev +401,LRL,Olmsted.Temp-Water.Inst.15Minutes.0.USGS-raw +402,LRL,Olmsted.Temp-Water.Inst.15Minutes.0.USGS-rev +403,LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-raw +404,LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-rev +417,LRL,Taylorsville.Elev.Inst.0.0.lrldlb-raw +418,LRL,Taylorsville.Elev.Inst.0.0.lrldlb-rev +419,LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-raw +420,LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-rev +421,LRL,Taylorsville.Elev.Inst.1Hour.0.LRL-cavi-fct +422,LRL,Taylorsville.Elev.Inst.1Hour.0.National-CWMS-Forecast +423,LRL,Taylorsville.Elev.Inst.1Hour.0.lrldlb-comp +424,LRL,Taylorsville.Elev.Inst.~1Day.0.LRL-cavi-fct +425,LRL,Taylorsville.Flow-BP1.Inst.0.0.lrldlb-comp +426,LRL,Taylorsville.Flow-BP1.Inst.1Hour.0.lrldlb-comp +427,LRL,Taylorsville.Flow-BP2.Inst.0.0.lrldlb-comp +428,LRL,Taylorsville.Flow-BP2.Inst.1Hour.0.lrldlb-comp +429,LRL,Taylorsville.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +430,LRL,Taylorsville.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +431,LRL,Taylorsville.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +432,LRL,Taylorsville.Flow-MG1.Inst.0.0.lrldlb-comp +433,LRL,Taylorsville.Flow-MG1.Inst.1Hour.0.lrldlb-comp +434,LRL,Taylorsville.Flow-MG2.Inst.0.0.lrldlb-comp +435,LRL,Taylorsville.Flow-MG2.Inst.1Hour.0.lrldlb-comp +436,LRL,Taylorsville.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +437,LRL,Taylorsville.Flow-Outflow.Inst.0.0.lrldlb-comp +438,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +439,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +440,LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +441,LRL,Taylorsville.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +442,LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-raw +443,LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-rev +444,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-fct +445,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-raw +446,LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-rev +447,LRL,Taylorsville.Opening-BP1.Inst.1Hour.0.lrldlb-comp +448,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-fct +449,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-raw +450,LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-rev +451,LRL,Taylorsville.Opening-BP2.Inst.1Hour.0.lrldlb-comp +452,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-fct +453,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-raw +454,LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-rev +455,LRL,Taylorsville.Opening-L1.Inst.1Hour.0.lrldlb-comp +456,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-fct +457,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-raw +458,LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-rev +459,LRL,Taylorsville.Opening-L2.Inst.1Hour.0.lrldlb-comp +460,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-fct +461,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-raw +462,LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-rev +463,LRL,Taylorsville.Opening-MG1.Inst.1Hour.0.lrldlb-comp +464,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-fct +465,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-raw +466,LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-rev +467,LRL,Taylorsville.Opening-MG2.Inst.1Hour.0.lrldlb-comp +468,LRL,Taylorsville.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +469,LRL,Taylorsville.Precip.Inst.15Minutes.0.LRGS-raw +470,LRL,Taylorsville.Precip.Inst.1Hour.0.National-CWMS-Forecast +471,LRL,Taylorsville.Precip.Total.15Minutes.15Minutes.USGS-raw +472,LRL,Taylorsville.Precip.Total.15Minutes.15Minutes.USGS-rev +473,LRL,Taylorsville.Precip.Total.1Hour.1Hour.Openweather +474,LRL,Taylorsville.Precip.Total.~1Day.1Day.lrldlb-raw +475,LRL,Taylorsville.Precip.Total.~1Day.1Day.lrldlb-rev +476,LRL,Taylorsville.Stage-Tailwater.Inst.0.0.lrldlb-raw +477,LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-raw +478,LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-rev +479,LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-raw +480,LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-rev +481,LRL,Taylorsville.Stor.Inst.1Hour.0.lrldlb-comp +482,LRL,Taylorsville.Temp-Air.Inst.1Hour.0.Openweather +483,LRL,Taylorsville.Temp-Air.Inst.~1Day.0.lrldlb-raw +484,LRL,Taylorsville.Temp-Air.Max.~1Day.1Day.lrldlb-raw +485,LRL,Taylorsville.Temp-Air.Min.~1Day.1Day.lrldlb-raw +486,LRL,Taylorsville.Temp-Water.Inst.~1Day.0.lrldlb-raw +506,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +507,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +508,LRL,DUNK2.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +509,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +510,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +511,LRL,DUNK2.Flow.Inst.6Hours.0.OHRFC-operQPF +533,LRL,Cincinnati.Elev.Inst.15Minutes.0.USGS-comp +534,LRL,Cincinnati.Flow.Inst.15Minutes.0.USGS-raw +535,LRL,Cincinnati.Flow.Inst.15Minutes.0.USGS-rev +536,LRL,Cincinnati.Stage.Inst.15Minutes.0.LRGS-raw +537,LRL,Cincinnati.Stage.Inst.15Minutes.0.LRGS-rev +538,LRL,Cincinnati.Stage.Inst.15Minutes.0.USGS-raw +539,LRL,Cincinnati.Stage.Inst.15Minutes.0.USGS-rev +540,LRL,Cincinnati.Stage.Inst.~1Day.0.lrldlb-raw +541,LRL,Cincinnati.Stage.Inst.~1Day.0.lrldlb-rev diff --git a/load_data/data/LRL_timeseries_ids_used.csv b/load_data/data/LRL_timeseries_ids_used.csv new file mode 100755 index 0000000000..4fe6290f0d --- /dev/null +++ b/load_data/data/LRL_timeseries_ids_used.csv @@ -0,0 +1,306 @@ +office,ts_id +LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Taylorsville-Lake.Elev.Inst.15Minutes.0.LRGS-rev +LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-raw +LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.LRGS-rev +LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-raw +LRL,Taylorsville-Lake.Elev.Inst.5Minutes.0.USGS-rev +LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.LRGS-comp +LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-raw +LRL,BowlingGreenKY.Flow.Inst.15Minutes.0.USGS-rev +LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-raw +LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.LRGS-rev +LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-raw +LRL,BowlingGreenKY.Stage.Inst.15Minutes.0.USGS-rev +LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-raw +LRL,BowlingGreenKY.Stage.Inst.~1Day.0.lrldlb-rev +LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Green-Lake.Elev.Inst.15Minutes.0.LRGS-rev +LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-raw +LRL,Green-Lake.Elev.Inst.5Minutes.0.LRGS-rev +LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-raw +LRL,Green-Lake.Elev.Inst.5Minutes.0.USGS-rev +LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Green-Lake.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-raw +LRL,Green-Lake.Stage.Inst.5Minutes.0.LRGS-rev +LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-raw +LRL,Green-Lake.Stage.Inst.5Minutes.0.USGS-rev +LRL,Green.Elev.Inst.0.0.lrldlb-raw +LRL,Green.Elev.Inst.0.0.lrldlb-rev +LRL,Green.Elev.Inst.0.0.radar-test +LRL,Green.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Green.Elev.Inst.15Minutes.0.LRGS-rev +LRL,Green.Elev.Inst.1Hour.0.LRL-cavi-fct +LRL,Green.Elev.Inst.1Hour.0.National-CWMS-Forecast +LRL,Green.Elev.Inst.1Hour.0.lrldlb-comp +LRL,Green.Elev.Inst.~1Day.0.LRL-cavi-fct +LRL,Green.Flow-BP1.Inst.0.0.lrldlb-comp +LRL,Green.Flow-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Green.Flow-BP2.Inst.0.0.lrldlb-comp +LRL,Green.Flow-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Green.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Green.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +LRL,Green.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Green.Flow-MG1.Inst.0.0.lrldlb-comp +LRL,Green.Flow-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Green.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Green.Flow-Outflow.Inst.0.0.lrldlb-comp +LRL,Green.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +LRL,Green.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Green.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +LRL,Green.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +LRL,Green.Opening-BP1.Inst.0.0.lrldlb-fct +LRL,Green.Opening-BP1.Inst.0.0.lrldlb-raw +LRL,Green.Opening-BP1.Inst.0.0.lrldlb-rev +LRL,Green.Opening-BP1.Inst.0.0.radar-test +LRL,Green.Opening-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Green.Opening-BP2.Inst.0.0.lrldlb-fct +LRL,Green.Opening-BP2.Inst.0.0.lrldlb-raw +LRL,Green.Opening-BP2.Inst.0.0.lrldlb-rev +LRL,Green.Opening-BP2.Inst.0.0.radar-test +LRL,Green.Opening-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Green.Opening-L1.Inst.0.0.lrldlb-fct +LRL,Green.Opening-L1.Inst.0.0.lrldlb-raw +LRL,Green.Opening-L1.Inst.0.0.lrldlb-rev +LRL,Green.Opening-L1.Inst.0.0.radar-test +LRL,Green.Opening-L1.Inst.1Hour.0.lrldlb-comp +LRL,Green.Opening-L2.Inst.0.0.lrldlb-fct +LRL,Green.Opening-L2.Inst.0.0.lrldlb-raw +LRL,Green.Opening-L2.Inst.0.0.lrldlb-rev +LRL,Green.Opening-L2.Inst.0.0.radar-test +LRL,Green.Opening-L2.Inst.1Hour.0.lrldlb-comp +LRL,Green.Opening-MG1.Inst.0.0.lrldlb-fct +LRL,Green.Opening-MG1.Inst.0.0.lrldlb-raw +LRL,Green.Opening-MG1.Inst.0.0.lrldlb-rev +LRL,Green.Opening-MG1.Inst.0.0.radar-test +LRL,Green.Opening-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Green.Opening-MG2.Inst.0.0.lrldlb-raw +LRL,Green.Opening-MG2.Inst.0.0.lrldlb-rev +LRL,Green.Opening-MG2.Inst.1Hour.0.lrldlb-comp +LRL,Green.Stage-Tailwater.Inst.0.0.lrldlb-raw +LRL,Green.Stage-Tailwater.Inst.0.0.radar-test +LRL,Green.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Green.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Green.Stor.Inst.1Hour.0.lrldlb-comp +LRL,Greensburg.Flow.Inst.15Minutes.0.LRGS-comp +LRL,Greensburg.Precip.Inst.15Minutes.0.LRGS-raw +LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-raw +LRL,Greensburg.Precip.Total.15Minutes.15Minutes.USGS-rev +LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Greensburg.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-raw +LRL,Greensburg.Stage.Inst.15Minutes.0.USGS-rev +LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-raw +LRL,Greensburg.Stage.Inst.~1Day.0.lrldlb-rev +LRL,Greensburg.Stage.Inst.~1Day.0.radar-test +LRL,Catawba.Flow.Inst.15Minutes.0.LRGS-comp +LRL,Catawba.Flow.Inst.15Minutes.0.USGS-raw +LRL,Catawba.Flow.Inst.15Minutes.0.USGS-rev +LRL,Catawba.Flow.Inst.~1Day.0.LRL-cavi-fct +LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Catawba.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Catawba.Stage.Inst.15Minutes.0.USGS-raw +LRL,Catawba.Stage.Inst.15Minutes.0.USGS-rev +LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-raw +LRL,Catawba.Stage.Inst.~1Day.0.lrldlb-rev +LRL,Buckhorn-Lake.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-raw +LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.LRGS-rev +LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-raw +LRL,Buckhorn-Lake.Elev.Inst.5Minutes.0.USGS-rev +LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Buckhorn-Lake.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-raw +LRL,Buckhorn-Lake.Stage.Inst.5Minutes.0.USGS-rev +LRL,TaylorsvilleOH.Elev.Inst.15Minutes.0.USGS-comp +LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-raw +LRL,TaylorsvilleOH.Flow.Inst.15Minutes.0.USGS-rev +LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-raw +LRL,TaylorsvilleOH.Stage.Inst.15Minutes.0.USGS-rev +LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +LRL,DUNK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +LRL,BKVI3.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +LRL,BKVI3.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-raw +LRL,Olmsted-HW.Stage.Inst.15Minutes.0.USGS-rev +LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-0hrQPF +LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-48hrQPF +LRL,ALVK2.Flow-Local.Inst.6Hours.0.OHRFC-operQPF +LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +LRL,ALVK2.Flow.Inst.6Hours.0.OHRFC-operQPF +LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-0hrQPF +LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-48hrQPF +LRL,ALVK2S.Flow.Inst.6Hours.0.OHRFC-operQPF +LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-0QPF +LRL,Cairo-CIRI2.Elev.Inst.6Hours.0.LMRFC-fct-48QPF +LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-0QPF +LRL,Cairo-CIRI2.Stage.Inst.6Hours.0.LMRFC-fct-48QPF +LRL,Cairo.Elev.Inst.1Hour.0.USGS-rev +LRL,Cairo.Precip.Inst.1Hour.0.LRGS-raw +LRL,Cairo.Stage.Inst.1Hour.0.LRGS-raw +LRL,Cairo.Stage.Inst.1Hour.0.LRGS-rev +LRL,Cairo.Stage.Inst.1Hour.0.USGS-raw +LRL,Cairo.Stage.Inst.1Hour.0.USGS-rev +LRL,Barkley-BARK2.Flow.Ave.~6Hours.6Hours.TVA-fct +LRL,Barkley-BARK2.Flow.Inst.1Hour.0.TVA-obs +LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-0hrQPF +LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-48hrQPF +LRL,GRLK2.Flow-ResIn.Inst.6Hours.0.OHRFC-operQPF +LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-0hrQPF +LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-48hrQPF +LRL,GRLK2.Flow.Inst.6Hours.0.OHRFC-operQPF +LRL,Buckhorn.Elev.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Elev.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Elev.Inst.0.0.radar-test +LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Buckhorn.Elev.Inst.15Minutes.0.LRGS-rev +LRL,Buckhorn.Elev.Inst.1Hour.0.LRL-cavi-fct +LRL,Buckhorn.Elev.Inst.1Hour.0.National-CWMS-Forecast +LRL,Buckhorn.Elev.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Elev.Inst.~1Day.0.LRL-cavi-fct +LRL,Buckhorn.Flow-BP1.Inst.0.0.lrldlb-comp +LRL,Buckhorn.Flow-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Flow-BP2.Inst.0.0.lrldlb-comp +LRL,Buckhorn.Flow-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Buckhorn.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +LRL,Buckhorn.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Buckhorn.Flow-MG1.Inst.0.0.lrldlb-comp +LRL,Buckhorn.Flow-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Buckhorn.Flow-Outflow.Inst.0.0.lrldlb-comp +LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Buckhorn.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-fct +LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-BP1.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-BP1.Inst.0.0.radar-test +LRL,Buckhorn.Opening-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-fct +LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-BP2.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-BP2.Inst.0.0.radar-test +LRL,Buckhorn.Opening-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-L1.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-L1.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-L2.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-L2.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-fct +LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-MG1.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-MG1.Inst.0.0.radar-test +LRL,Buckhorn.Opening-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Opening-MG2.Inst.0.0.lrldlb-rev +LRL,Buckhorn.Opening-MG2.Inst.1Hour.0.lrldlb-comp +LRL,Buckhorn.Precip-Loss.Inst.1Hour.0.National-CWMS-Forecast +LRL,Buckhorn.Precip.Inst.15Minutes.0.LRGS-raw +LRL,Buckhorn.Precip.Inst.1Hour.0.National-CWMS-Forecast +LRL,Buckhorn.Precip.Total.1Hour.1Hour.Openweather +LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-raw +LRL,Buckhorn.Precip.Total.~1Day.1Day.lrldlb-rev +LRL,Buckhorn.Precip.Total.~1Day.1Day.radar-test +LRL,Buckhorn.Stage-Tailwater.Inst.0.0.lrldlb-raw +LRL,Buckhorn.Stage-Tailwater.Inst.0.0.radar-test +LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-raw +LRL,Buckhorn.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Buckhorn.Stor.Inst.1Hour.0.lrldlb-comp +LRL,Olmsted-OLMI2.Elev-Headwater.Inst.15Minutes.0.USGS-comp +LRL,Olmsted-OLMI2.Elev-Tailwater.Inst.15Minutes.0.USGS-comp +LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-raw +LRL,Olmsted-OLMI2.Flow.Ave.15Minutes.15Minutes.USGS-rev +LRL,Olmsted-OLMI2.Opening-Gate-Average.Inst.1Hour.0.LPMS-comp +LRL,Olmsted-OLMI2.Opening-Gate-Total.Inst.0.0.LPMS-raw +LRL,Olmsted-OLMI2.Precip.Total.0.1Day.LPMS-raw +LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-raw +LRL,Olmsted-OLMI2.Precip.Total.15Minutes.15Minutes.USGS-rev +LRL,Olmsted-OLMI2.Stage-Headwater.Inst.0.0.LPMS-raw +LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-raw +LRL,Olmsted-OLMI2.Stage-Headwater.Inst.15Minutes.0.USGS-rev +LRL,Olmsted-OLMI2.Stage-Headwater.Inst.1Day.0.LPMS-comp +LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.0.0.LPMS-raw +LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-raw +LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.15Minutes.0.USGS-rev +LRL,Olmsted-OLMI2.Stage-Tailwater.Inst.1Day.0.LPMS-comp +LRL,Olmsted-OLMI2.Temp-Air.Inst.0.0.LPMS-raw +LRL,Olmsted-OLMI2.Temp-Water.Inst.0.0.LPMS-raw +LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-raw +LRL,Olmsted-OLMI2.Temp-Water.Inst.15Minutes.0.USGS-rev +LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-raw +LRL,Olmsted-TW.Stage.Inst.15Minutes.0.USGS-rev +LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-raw +LRL,Olmsted.Flow.Ave.15Minutes.15Minutes.USGS-rev +LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-raw +LRL,Olmsted.Precip.Total.15Minutes.15Minutes.USGS-rev +LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-raw +LRL,Olmsted.Stage-Tailwater.Inst.15Minutes.0.LRGS-rev +LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-raw +LRL,OlmstedUpr.Stage.Inst.15Minutes.0.LRGS-rev +LRL,Taylorsville.Elev.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Elev.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-raw +LRL,Taylorsville.Elev.Inst.15Minutes.0.LRGS-rev +LRL,Taylorsville.Elev.Inst.1Hour.0.LRL-cavi-fct +LRL,Taylorsville.Elev.Inst.1Hour.0.National-CWMS-Forecast +LRL,Taylorsville.Elev.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Elev.Inst.~1Day.0.LRL-cavi-fct +LRL,Taylorsville.Flow-BP1.Inst.0.0.lrldlb-comp +LRL,Taylorsville.Flow-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Flow-BP2.Inst.0.0.lrldlb-comp +LRL,Taylorsville.Flow-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Flow-Inflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Taylorsville.Flow-Inflow.Ave.1Hour.6Hours.lrldlb-comp +LRL,Taylorsville.Flow-Inflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Taylorsville.Flow-MG1.Inst.0.0.lrldlb-comp +LRL,Taylorsville.Flow-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Flow-MG2.Inst.0.0.lrldlb-comp +LRL,Taylorsville.Flow-MG2.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Flow-Outflow.Ave.1Hour.1Hour.lrldlb-comp +LRL,Taylorsville.Flow-Outflow.Inst.0.0.lrldlb-comp +LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.LRL-cavi-fct +LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.National-CWMS-Forecast +LRL,Taylorsville.Flow-Outflow.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Flow-Outflow.Inst.~1Day.0.LRL-cavi-fct +LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-raw +LRL,Taylorsville.Flow.Inst.15Minutes.0.USGS-rev +LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-BP1.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-BP1.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-BP2.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-BP2.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-L1.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-L1.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-L2.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-L2.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-MG1.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-MG1.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-fct +LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Opening-MG2.Inst.0.0.lrldlb-rev +LRL,Taylorsville.Opening-MG2.Inst.1Hour.0.lrldlb-comp +LRL,Taylorsville.Stage-Tailwater.Inst.0.0.lrldlb-raw +LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-raw +LRL,Taylorsville.Stage.Inst.15Minutes.0.USGS-rev +LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-raw +LRL,Taylorsville.Stage.Inst.~1Day.0.lrldlb-rev +LRL,Taylorsville.Stor.Inst.1Hour.0.lrldlb-comp diff --git a/load_data/data/LRL_timeseries_values_melted.parquet b/load_data/data/LRL_timeseries_values_melted.parquet new file mode 100755 index 0000000000..45dec59c4f Binary files /dev/null and b/load_data/data/LRL_timeseries_values_melted.parquet differ diff --git a/load_data/data/MVP_locations_data.csv b/load_data/data/MVP_locations_data.csv new file mode 100755 index 0000000000..10b2d6e408 --- /dev/null +++ b/load_data/data/MVP_locations_data.csv @@ -0,0 +1,171 @@ +office,name,nearest-city,public-name,kind,time-zone,latitude,longitude,unit,nation,state,county,bounding-office,active,aliases,long-name,map-label,type,published-latitude,published-longitude,horizontal-datum,elevation,vertical-datum,description +MVP,LockDam_05-TainterGate23,Winona,Lock and Dam 05 Tainter Gate 23,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate23'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate23'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate23'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate23'}]",Mississippi River Lock and Dam 05 Tainter Gate 23,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,TraverseWR_Dam-MainLake,Fergus Falls,"Mud Lake North of 670TH Street Near Wheaton, MN",SITE,US/Central,45.836055555556,-96.572944444444,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-MainLake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-MainLake'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-MainLake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049900'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-MainLake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-MainLake'}]",White Rock Dam Main Lake Gage,White Rock Dam Main Lake Gage,,0.0,0.0,NAD83,899.9999999999999,LOCAL,"MUD LAKE NORTH OF 670TH STREET NEAR WHEATON, MN" +MVP,Highway75_Dam,Odessa,Highway 75 Dam,PROJECT,US/Central,45.248301,-96.29788,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam'}]",Highway 75 Dam at Big Stone Lake and Whetstone River Project,Highway 75 Dam,Dam,0.0,0.0,NAD83,0.0,NGVD29,USACE Owned and Maintained +MVP,Baldhill_Dam-ServiceSpillway,Jamestown,Baldhill Dam Service Spillway,SITE,US/Central,47.0356524,-98.0813989,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-ServiceSpillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-ServiceSpillway'}]",,Baldhill Dam Service Spillway,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-SlideGate01,Brainerd,Gull Slide Gate 01,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate01'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate25,Winona,Lock and Dam 05 Tainter Gate 25,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate25'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate25'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate25'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate25'}]",Mississippi River Lock and Dam 05 Tainter Gate 25,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-Tailwater,Winona,Lock and Dam 05a Tailwater,SITE,US/Central,44.085038,-91.667549,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05378490'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Tailwater'}]",Mississippi River Lock and Dam 05a Tailwater,Mississippi River Lock and Dam 05a Tailwater,,0.0,0.0,NAD83,0.0,NAVD88, +MVP,LockDam_05-TainterGate27,Winona,Lock and Dam 05 Tainter Gate 27,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate27'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate27'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate27'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate27'}]",Mississippi River Lock and Dam 05 Tainter Gate 27,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-CP_LD05TW,Winona,Lock Dam 05a Ctrl Point,SITE,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-CP_LD05TW'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-CP_LD05TW'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-CP_LD05TW'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-CP_LD05TW'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ZUMM5,Rochester,Zumbro River at Zumbro Falls,SITE,US/Central,44.2867,-92.4322,m,United States,MN,Wabasha,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05374000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ZUMM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ZUMM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ZUMM5'}]","Zumbro River at Zumbro Falls, MN",,DCP Gage,,,,247.272048,LOCAL, +MVP,MissHW_PineRiver-SlideGate04,Brainerd,Pine River Dam Slide Gate 04,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate04'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-TainterGate01,Fergus Falls,White Rock Dam Tainter Gate 01,OUTLET,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate01'}]",,White Rock Dam Tainter Gate,,,,NAD83,274.32,LOCAL, +MVP,Highway75_Dam-LowFlow,Watertown,Highway 75 Dam Low Flow,OUTLET,US/Central,45.248536111111,-96.297127777778,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LowFlow'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LowFlow'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LowFlow'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LowFlow'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ODAM5'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LFKM5S,Hibbing,Little Fork Snow,SITE,US/Central,48.39861,-93.56722,ft,United States,MN,Koochiching,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LFKM5S'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LFKM5S'}]",Little Fork Snow,Little Fork Snow,,,,,,, +MVP,LockDam_05-TainterGates,Winona,Lock and Dam 05 Tainter Gates,SITE,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGates'}]",Mississippi River Lock and Dam 05 Tainter Gates,Mississippi River Lock and Dam 05 Tainter Gates,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-RollerGate01,Winona,Lock and Dam 05a Roller Gate 01,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate01'}]",Mississippi River Lock and Dam 05a Roller Gate 01,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate02,Brainerd,Pine River Dam Slide Gate 02,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate02'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,TraverseWR_Dam-TainterGate03,Fergus Falls,White Rock Dam Tainter Gate 03,OUTLET,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate03'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate03'}]",,White Rock Dam Tainter Gate,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_02-HydroTurbines,Hastings,Lock and Dam 02 Hydro Turbines,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-HydroTurbines'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-HydroTurbines'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-HydroTurbines'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-HydroTurbines'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver,Crosslake,Pine River Dam at Cross Lake,PROJECT,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver'}]",Mississippi River Headwaters Pine River Dam at Cross Lake,Pine River Dam at Cross Lake,Dam,,,NAD83,1199.9999999999998,NGVD29,USACE Owned and Maintained +MVP,LockDam_02-TainterValves,Hastings,Lock and Dam 02 Tainter Valves,SITE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterValves'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterValves'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate13,Winona,Lock and Dam 05 Tainter Gate 13,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate13'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate13'}]",Mississippi River Lock and Dam 05 Tainter Gate 13,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_Gull-Lake,Brainerd,Gull Lake near Brainerd,SITE,US/Central,46.410931,-94.359777,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLKM5'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Lake'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Lake'}]",,Gull Lake Dam,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-Tailwater,Brainerd,Gull Lake Dam Tailwater,SITE,US/Central,46.410869,-94.353622,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Tailwater'}]",,Gull Lake Dam,,,,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate15,Winona,Lock and Dam 05 Tainter Gate 15,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate15'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate15'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate15'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate15'}]",Mississippi River Lock and Dam 05 Tainter Gate 15,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_Gull-SlideGate03,Brainerd,Gull Slide Gate 03,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate03'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate17,Winona,Lock and Dam 05 Tainter Gate 17,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate17'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate17'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate17'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate17'}]",Mississippi River Lock and Dam 05 Tainter Gate 17,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Highway75_Dam-LowFlow-Tailwater,Watertown,Highway 75 Dam Low Flow Tailwater,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LowFlow-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LowFlow-Tailwater'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LowFlow-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LowFlow-Tailwater'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate19,Winona,Lock and Dam 05 Tainter Gate 19,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate19'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate19'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate19'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate19'}]",Mississippi River Lock and Dam 05 Tainter Gate 19,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,DAWM5,Marshall,W Br Lac Qui Parle nr Dawson,SITE,US/Central,44.9297,-96.0514,ft,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'Agency Aliases-MNDNR', 'value': '24059001'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'DAWM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'DAWM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'DAWM5'}]","West Branch Lac Qui Parle River near Dawson, MN",,DCP Gage,,,,,,Owner is MNDNR +MVP,Highway75_Dam-Tailwater,Watertown,Highway 75 Dam Tailwater,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-Tailwater'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-Tailwater'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-SlideGate05,Brainerd,Gull Slide Gate 05,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate05'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_05-TainterGate21,Winona,Lock and Dam 05 Tainter Gate 21,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate21'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate21'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate21'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate21'}]",Mississippi River Lock and Dam 05 Tainter Gate 21,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate13,Cottage Grove,Lock and Dam 02 Tainter Gate 13,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate13'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate32,Winona,Lock and Dam 05 Tainter Gate 32,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate32'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate32'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate32'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate32'}]",Mississippi River Lock and Dam 05 Tainter Gate 32,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-FishPondSiphon,Jamestown,Baldhill Dam Fish Pond Outlet,OUTLET,US/Central,47.033886,-98.0771,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-FishPondSiphon'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-FishPondSiphon'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-FishPondSiphon'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-FishPondSiphon'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-FishPondSiphon'}]",,Baldhill Dam Fish Pond Outlet,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate33,Winona,Lock and Dam 05 Tainter Gate 33,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate33'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate33'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate33'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate33'}]",Mississippi River Lock and Dam 05 Tainter Gate 33,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate15,Hastings,Lock and Dam 02 Tainter Gate 15,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate15'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate15'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate15'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate15'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-Tailwater,Valley City,Baldhill Dam Tailwater,SITE,US/Central,47.032919,-98.083911,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BHTN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05058000'}]",Baldhill Dam Tailwater,Baldhill Dam Tailwater,,0.0,0.0,NAD83,365.76,NGVD29, +MVP,LockDam_02-TainterGate17,Hastings,Lock and Dam 02 Tainter Gate 17,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate17'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate17'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate17'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate17'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate34,Winona,Lock and Dam 05 Tainter Gate 34,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate34'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate34'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate34'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate34'}]",Mississippi River Lock and Dam 05 Tainter Gate 34,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate29,Winona,Lock and Dam 05 Tainter Gate 29,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate29'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate29'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate29'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate29'}]",Mississippi River Lock and Dam 05 Tainter Gate 29,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate07,Winona,Lock and Dam 05 Tainter Gate 07,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate07'}]",Mississippi River Lock and Dam 05 Tainter Gate 07,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate30,Winona,Lock and Dam 05 Tainter Gate 30,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate30'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate30'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate30'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate30'}]",Mississippi River Lock and Dam 05 Tainter Gate 30,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate19,Hastings,Lock and Dam 02 Tainter Gate 19,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate19'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate19'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate19'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate19'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Clayton,Clayton,"Mississippi River at Clayton, IA",STREAM_LOCATION,America/Chicago,42.903611111111,-91.145,m,United States,IA,Clayton,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05411500'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'CLAI4'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CLAI4'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'CLAI4'}]","Mississippi River at Clayton, IA (CP10)","Mississippi River at Clayton, IA",DCP Gage,,,NAD83,600.0,LOCAL, +MVP,Baldhill_Dam-TainterGate02,Jamestown,Baldhill Dam Tainter Gate 02,OUTLET,US/Central,47.0361844,-98.081466,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate02'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate02'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,Highway75_Dam-LeafGate,Watertown,Highway 75 Dam Leaf Gate,OUTLET,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-LeafGate'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-LeafGate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-LeafGate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-LeafGate'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate09,Winona,Lock and Dam 05 Tainter Gate 09,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate09'}]",Mississippi River Lock and Dam 05 Tainter Gate 09,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate31,Winona,Lock and Dam 05 Tainter Gate 31,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate31'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate31'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate31'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate31'}]",Mississippi River Lock and Dam 05 Tainter Gate 31,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate11,Winona,Lock and Dam 05 Tainter Gate 11,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate11'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate11'}]",Mississippi River Lock and Dam 05 Tainter Gate 11,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Highway75_Dam-EmergencySpillway,Watertown,,SITE,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-EmergencySpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-EmergencySpillway'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-EmergencySpillway'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05a-TainterValves,Winona,Lock and Dam 05a Tainter Valves,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterValves'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterValves'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-RollerGate04,Winona,Lock and Dam 05a Roller Gate 04,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate04'}]",Mississippi River Lock and Dam 05a Roller Gate 04,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05,Minnesota City,Lock and Dam 5,PROJECT,US/Central,44.1609167,-91.8142361,m,United States,MN,Winona,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589'}]",Lock and Dam 05 at Mississippi River 9 foot Channel Navigation Project,Lock and Dam 05,Dam,0.0,0.0,NAD83,182.88,LOCAL,USACE Owned and Maintained +MVP,MissHW_PineRiver-Bays05_06,Brainerd,Pine River Dam Bays 5 & 6 - Slide Gates,SITE,US/Central,46.66914,-94.112745,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays05_06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays05_06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays05_06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays05_06'}]",Pine River Dam Bays 5 & 6 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 5, 6" +MVP,MissHW_PineRiver-SlideGate10,Brainerd,Pine River Dam Slide Gate 10,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate10'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_02-Turbine01,Hastings,Lock and Dam 02 Turbine 01,TURBINE,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Turbine01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Turbine01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Turbine01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Turbine01'}]",,Mississippi River Lock and Dam 02 Turbine 01,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-TainterGate08,Winona,Lock and Dam 05a Tainter Gate 08,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate08'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Lock_02,Hastings,Lock and Dam 02 Lock 02,LOCK,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Lock_02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Lock_02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Lock_02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Lock_02'}]",,Mississippi River Lock and Dam 02,Lock,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-TainterGate09,Winona,Lock and Dam 05a Tainter Gate 09,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate09'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate09'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate03,Hastings,Lock and Dam 02 Tainter Gate 03,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate03'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate08,Brainerd,Pine River Dam Slide Gate 08,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate08'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05a-Lock_01,Winona,Lock Dam 05a Lock 01,LOCK,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Lock_01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Lock_01'}]",,Mississippi River Lock and Dam 05a,Lock,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate05,Hastings,Lock and Dam 02 Tainter Gate 05,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate05'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-Bays01_02,Brainerd,Pine River Dam Bays 1 & 2 - Slide Gates,SITE,US/Central,46.669194,-94.112572,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays01_02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays01_02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays01_02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays01_02'}]",Pine River Dam Bays 1 & 2 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD 83,0.0,NGVD29,"Total opening of gates in bays 1, 2" +MVP,LockDam_02-TainterGates,Hastings,Lock and Dam 02 Tainter Gates,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGates'}]",,Mississippi River Lock and Dam 02 Tainter Gates,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate01,Winona,Lock and Dam 05 Roller Gate 01,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate01'}]",Mississippi River Lock and Dam 05 Roller Gate 01,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate06,Brainerd,Pine River Dam Slide Gate 06,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate06'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,Baldhill_Dam,Valley City,Baldhill Dam at Lake Ashtabula,PROJECT,US/Central,47.0361833,-98.0814667,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'ND00309'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam'}]","Baldhill Dam at Lake Ashtabula near Valley City, ND",Baldhill Dam,Dam,,,NAD83,0.0,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05a-TainterGate10,Winona,Lock and Dam 05a Tainter Gate 10,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate10'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate10'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate07,Hastings,Lock and Dam 02 Tainter Gate 07,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate07'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate07'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-TainterGate09,Hastings,Lock and Dam 02 Tainter Gate 09,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate09'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate09'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-Powerhouse,Hastings,Lock and Dam 02 Powerhouse,SITE,US/Central,44.760148,-92.867056,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Powerhouse'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Powerhouse'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Powerhouse'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Powerhouse'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,1968.5039370078734,NAVD88, +MVP,Rafferty_Dam-Tailwater,Williston,Rafferty Dam Tailwater,SITE,US/Central,49.1433333,-103.0830556,ft,Canada,00,Unknown County or County N/A for Unknown State or State N/A,,True,"[{'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB032-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'RAFQ8-Tailwater'}, {'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB036'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Rafferty_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Rafferty_Dam-Tailwater'}]","Souris River below Rafferty Reservoir, SK",Souris River b Rafferty RSVR,DCP Gage,,,,,,Owner is Environment Canada +MVP,LockDam_05a-TainterGate06,Winona,Lock and Dam 05a Tainter Gate 06,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate06'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-RollerGate03,Winona,Lock and Dam 05 Roller Gate 03,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate03'}]",Mississippi River Lock and Dam 05 Roller Gate 03,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Cooperstown200,Cooperstown,"SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN, ND",SITE,US/Central,,,ft,United States,ND,Griggs,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Cooperstown2'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05056995'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CPPN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Cooperstown2'}]","SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN, ND",SHEYENNE RIVER ON HWY 200 NEAR COOPERSTOWN,DCP Gage,,,NAD83,1272.9396325459318,NAVD88,"USACE Owned, USGS Maintained" +MVP,LockDam_05a-RollerGates,Winona,Lock and Dam 05a Roller Gates,SITE,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGates'}]",Mississippi River Lock and Dam 05a Roller Gates,Mississippi River Lock and Dam 05a Roller Gates,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a-TainterGate07,Winona,Lock and Dam 05a Tainter Gate 07,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGate07'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGate07'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate05,Winona,Lock and Dam 05 Roller Gate 05,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate05'}]",Mississippi River Lock and Dam 05 Roller Gate 05,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-SlideGate12,Brainerd,Pine River Dam Slide Gate 12,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate12'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate12'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,Highway75_Dam-ServiceSpillway-Gate,Watertown,,OUTLET,US/Central,45.2483,-96.2916,ft,United States,MN,Big Stone,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00581-ServiceSpillway-Gate'}, {'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-ServiceSpillway-Gate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-ServiceSpillway-Gate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-ServiceSpillway-Gate'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,ChippewaDiv_Dam-LowFlow,Watson,Chippewa Diversion Dam Low Flow,OUTLET,US/Central,45.021908,-95.792215,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-LowFlow'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578-LowFlow'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-LowFlow'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-LowFlow'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-LowFlow'}]",Chippewa Diversion Dam Low Flow Outlet,Chippewa Diversion Dam Low Flow,Gate,0.0,0.0,NAD83,0.0,NGVD29, +MVP,MissHW_PineRiver-Bays09_10,Brainerd,Pine River Dam Bays 9 & 10 - Slide Gates,SITE,US/Central,46.669089,-94.112926,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays09_10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays09_10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays09_10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays09_10'}]",Pine River Dam Bays 9 & 10 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 9, 10" +MVP,LockDam_02-TainterGate11,Hastings,Lock and Dam 02 Tainter Gate 11,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate11'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate11'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,TraverseWR_Dam-Stoplogs,Fergus Falls,White Rock Dam Stoplogs,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Stoplogs'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Stoplogs'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Stoplogs'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Stoplogs'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Stoplogs'}]",White Rock Dam Stoplogs,White Rock Dam Stoplogs,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_05-TainterGate24,Winona,Lock and Dam 05 Tainter Gate 24,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate24'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate24'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate24'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate24'}]",Mississippi River Lock and Dam 05 Tainter Gate 24,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ChippewaDiv_Dam-TainterGate,Watson,,OUTLET,US/Central,45.0236111,-95.7902778,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00578-TainterGate'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-TainterGate'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-TainterGate'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-TainterGate'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-TainterGate'}]",,Chippewa Diversion Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate26,Winona,Lock and Dam 05 Tainter Gate 26,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate26'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate26'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate26'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate26'}]",Mississippi River Lock and Dam 05 Tainter Gate 26,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,DAWM5S,Marshall,Dawson Snow,SITE,US/Central,44.82,-96.08,m,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'DAWM5S'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'DAWM5S'}]",Dawson Snow,Dawson Snow,,,,,,, +MVP,LockDam_05-TainterGate28,Winona,Lock and Dam 05 Tainter Gate 28,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate28'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate28'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate28'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate28'}]",Mississippi River Lock and Dam 05 Tainter Gate 28,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ELZM5,Elizabeth,Otter Tail River abv Elizabeth,SITE,US/Central,46.369444444444,-96.017222222222,m,United States,MN,Otter Tail,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05030500'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ELZM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ELZM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ELZM5'}]","Otter Tail River near Elizabeth, MN",,DCP Gage,,,NAD83,381.0,LOCAL,"Otter Tail River near Elizabeth, MN" +MVP,LockDam_05-RollerGates,Winona,Lock and Dam 05 Roller Gates,SITE,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGates'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGates'}]",Mississippi River Lock and Dam 05 Roller Gates,Mississippi River Lock and Dam 05 Roller Gates,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate03,Brainerd,Pine River Dam Slide Gate 03,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate03'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-Lake,Fergus Falls,White Rock Dam Lake Gage,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Lake'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Lake'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Lake'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Lake'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Lake'}]",White Rock Dam Lake Gage,White Rock Dam Lake Gage,,0.0,0.0,NAD83,899.9999999999999,LOCAL, +MVP,MissHW_PineRiver-SlideGate01,Brainerd,Pine River Dam Slide Gate 01,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate01'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,TraverseWR_Dam-TainterGate02,Fergus Falls,White Rock Dam Tainter Gate 02,OUTLET,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-TainterGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-TainterGate02'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-TainterGate02'}]",,White Rock Dam Tainter Gate,,,,NAD83,274.32,LOCAL, +MVP,LockDam_02-TainterGate01,Hastings,Lock and Dam 02 Tainter Gate 01,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate01'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-RollerGate02,Winona,Lock and Dam 05a Roller Gate 02,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate02'}]",Mississippi River Lock and Dam 05a Roller Gate 02,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Baldhill_Dam-LowFlow02,Valley City,Baldhill Dam Low Flow Gate 02,OUTLET,US/Central,47.0358647,-98.0810972,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-LowFlow02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-LowFlow02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-LowFlow02'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-LowFlow02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-LowFlow02'}]",Baldhill Dam Low Flow Gate 01,Baldhill Dam Low Flow Gate,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_05-TainterGate12,Winona,Lock and Dam 05 Tainter Gate 12,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate12'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate12'}]",Mississippi River Lock and Dam 05 Tainter Gate 12,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,ChippewaDiv_Dam-Tailwater,Watson,Chippewa Diversion Dam Tailwater,SITE,US/Central,45.0236111,-95.7902778,ft,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05304995-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05305000'}]",,Chippewa Diversion Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_05-TainterGate14,Winona,Lock and Dam 05 Tainter Gate 14,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate14'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate14'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate14'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate14'}]",Mississippi River Lock and Dam 05 Tainter Gate 14,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,TraverseWR_Dam,Wheaton,White Rock Dam at Mud Lake Reservoir,PROJECT,US/Central,45.8616,-96.5716,m,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577'}]",White Rock Dam at Mud Lake Reservoir at Lake Traverse Project,White Rock Dam at Mud Lake Reservoir,Dam,,,NAD83,274.32,LOCAL,"USACE Owned, USGS Maintained" +MVP,Cooperstown,Jamestown,"Sheyenne River at Cooperstown, ND",SITE,US/Central,47.4336,-98.0286,m,United States,ND,Griggs,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Cooperstown'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CPRN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Cooperstown'}]","Sheyenne River at Cooperstown, ND",,DCP Gage,,,NAD27,387.632448,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05-TainterGate16,Winona,Lock and Dam 05 Tainter Gate 16,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate16'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate16'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate16'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate16'}]",Mississippi River Lock and Dam 05 Tainter Gate 16,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MOOM5S,Cloquet,Moose Lake Snow,SITE,US/Central,46.43639,-92.70611,m,United States,MN,Kanabec,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MOOM5S'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MOOM5S'}]",Moose Lake Snow,Moose Lake Snow,,,,,,, +MVP,ChippewaDiv_Dam,Watson,Chippewa Diversion Dam,PROJECT,US/Central,45.022025,-95.791308333333,m,United States,MN,Chippewa,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WTSM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05304995'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ChippewaDiv_Dam'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ChippewaDiv_Dam'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00578'}]",,Chippewa Diversion Dam,Dam,0.0,0.0,NAD83,0.0,NGVD29,"USACE Owned, USGS Maintained" +MVP,LockDam_05-TainterGate18,Winona,Lock and Dam 05 Tainter Gate 18,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate18'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate18'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate18'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate18'}]",Mississippi River Lock and Dam 05 Tainter Gate 18,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Muscoda,Middleton,"Wisconsin River at Muscoda, WI",SITE,US/Central,43.1983,-90.4406,m,United States,WI,Grant,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05407000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MUSW3'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MUSW3'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MUSW3'}]","Wisconsin River at Muscoda, WI","Wisconsin River at Muscoda, WI",DCP Gage,,,NAD83,203.231496,LOCAL, +MVP,LockDam_05-Tailwater,Winona,Lock and Dam 5 Tailwater,SITE,US/Central,44.158307,-91.808447,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-Tailwater'}]",Mississippi River Lock and Dam 05 Tailwater,Mississippi River Lock and Dam 05 Tailwater,,0.0,0.0,NAD83,0.0,NAVD88, +MVP,MissHW_Gull-SlideGate04,Brainerd,Gull Slide Gate 04,OUTLET,US/Central,46.4116,-94.3533,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate04'}]",,Gull Lake Dam,,,,NAD83,1099.9999999999998,NGVD29, +MVP,MissHW_Gull-SlideGate02,Brainerd,Gull Slide Gate 02,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-SlideGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-SlideGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-SlideGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-SlideGate02'}]",,Gull Lake Dam,,0.0,,NAD83,0.0,NGVD29, +MVP,Baldhill_Dam-EmergencySpillway,Jamestown,Baldhill Dam Emergency Spillway,SITE,US/Central,47.0346415,-98.0788482,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-EmergencySpillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-EmergencySpillway'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-EmergencySpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-EmergencySpillway'}]",,Baldhill Dam Emergency Spillway,,,,NAD83,0.0,NGVD29, +MVP,MissHW_Gull-StopLog01,Brainerd,Gull Stop Log 01,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-StopLog01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-StopLog01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-StopLog01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00596-StopLog01'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_05-TainterGate20,Winona,Lock and Dam 05 Tainter Gate 20,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate20'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate20'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate20'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate20'}]",Mississippi River Lock and Dam 05 Tainter Gate 20,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-Spillway,Hastings,Lock and Dam 02 Spillway,SITE,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Spillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Spillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Spillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Spillway'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate22,Winona,Lock and Dam 05 Tainter Gate 22,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate22'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate22'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate22'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate22'}]",Mississippi River Lock and Dam 05 Tainter Gate 22,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,NWUM5,New Ulm,Cottonwood River at New Ulm,SITE,US/Central,44.2914,-94.44,m,United States,MN,Brown,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'NWUM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'NWUM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05317000'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'NWUM5'}]",Cottonwood River at New Ulm. MN,,DCP Gage,,,,242.87378400000003,LOCAL, +MVP,NWUM5S,New Ulm,New Ulm Snow,SITE,US/Central,44.32111,-94.43889,m,United States,MN,Brown,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'NWUM5S'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'NWUM5S'}]",New Ulm Snow,New Ulm Snow,,,,,,, +MVP,LockDam_02-TainterGate12,Hastings,Lock and Dam 02 Tainter Gate 12,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate12'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate12'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate12'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate12'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,TraverseWR_Dam-Tailwater,Fergus Falls,White Rock Dam Tailwater,SITE,US/Central,45.8616,-96.5716,ft,United States,MN,Traverse,MVP,True,"[{'name': 'Agency Aliases-USGS Station Number', 'value': '05050000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WHRM5-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00577-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'TraverseWR_Dam-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'TraverseWR_Dam-Tailwater'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05049995-Tailwater'}]",White Rock Dam Tailwater,White Rock Dam Tailwater,,,,NAD83,899.9999999999999,LOCAL, +MVP,LockDam_02-TainterGate14,Hastings,Lock and Dam 02 Tainter Gate 14,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate14'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate14'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate14'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate14'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,AGYM5,Argyle,Middle River at Argyle,SITE,US/Central,48.340277777778,-96.816111111111,ft,United States,MN,Marshall,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'AGYM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'AGYM5'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'AGYM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05087500'}]","Middle River at Argyle, MN",,DCP Gage,,,NAD83,828.5299999999999,LOCAL,"Middle River at Argyle, MN" +MVP,LockDam_02-TainterGate16,Hastings,Lock and Dam 02 Tainter Gate 16,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate16'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate16'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate16'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate16'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate18,Hastings,Lock and Dam 02 Tainter Gate 18,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate18'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate18'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate18'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate18'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-CP_Alma,Winona,Lock and Dam 05 Control Point,SITE,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-CP_Alma'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-CP_Alma'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-CP_Alma'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-CP_Alma'}]",,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LFKM5,Hibbing,Little Fork River at Little Fk,SITE,US/Central,48.3958,-93.5492,ft,United States,MN,Koochiching,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LFKM5'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'LFKM5'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05131500'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LFKM5'}]","Little Fork River at Little Fork, MN",,DCP Gage,,,,1083.59,LOCAL, +MVP,LockDam_05a-Spillway,Winona,Lock and Dam 05a Spillway,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-Spillway'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-Spillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-Spillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-Spillway'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-TainterGate08,Winona,Lock and Dam 05 Tainter Gate 08,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate08'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate08'}]",Mississippi River Lock and Dam 05 Tainter Gate 08,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05-TainterGate10,Winona,Lock and Dam 05 Tainter Gate 10,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterGate10'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterGate10'}]",Mississippi River Lock and Dam 05 Tainter Gate 10,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Baldhill_Dam-TainterGate01,Jamestown,Baldhill Dam Tainter Gate 01,OUTLET,US/Central,47.0361844,-98.081466,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate01'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate01'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,ABRN8,Abercrombie,Wild Rice River at Abercrombie,STREAM_LOCATION,US/Central,46.4680556,-96.7833333,ft,United States,ND,Richland,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'ABRN8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'ABRN8'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05053000'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ABRN8'}]","Wild Rice River near Abercrombie, ND",,,,,NAD83,907.9399999999999,,"Wild Rice River near Abercrombie, ND" +MVP,Baldhill_Dam-LowFlow01,Valley City,Baldhill Dam Low Flow Gate 01,OUTLET,US/Central,47.0359542,-98.0812555,m,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-LowFlow01'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-LowFlow01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-LowFlow01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-LowFlow01'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-LowFlow01'}]",Baldhill Dam Low Flow Gate 01,Baldhill Dam Low Flow Gate 01,,,,NAD83,365.76,NGVD29, +MVP,LockDam_02-Tailwater,Hastings,Lock and Dam 2 Tailwater,SITE,US/Central,44.757049,-92.866069,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Tailwater'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Tailwater'}]",Mississippi River Lock and Dam 02 Tailwater,Mississippi River Lock and Dam 02 Tailwater,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,Baldhill_Dam-TainterGate03,Jamestown,Baldhill Dam Tainter Gate 03,OUTLET,US/Central,47.0361844,-98.081466,ft,United States,ND,Barnes,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'BLDN8-TainterGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'ND00309-TainterGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Baldhill_Dam-TainterGate03'}, {'name': 'Agency Aliases-USGS Station Number', 'value': '05057500-TainterGate03'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Baldhill_Dam-TainterGate03'}]",,Baldhill Dam,,,,NAD83,0.0,NGVD29, +MVP,Highway75_Dam-ServiceSpillway,Watertown,Highway 75 Dam Service Spillway,SITE,US/Central,45.226761111111,-96.290252777778,ft,United States,MN,Lac Qui Parle,MVP,True,"[{'name': 'RFC CHPS Aliases-Locations', 'value': 'ODAM5_CHPS-ServiceSpillway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'ODEM5'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00581-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Highway75_Dam-ServiceSpillway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Highway75_Dam-ServiceSpillway'}]",,Highway 75 Dam,,0.0,0.0,NAD83,0.0,NGVD29, +MVP,LockDam_10-CP_Clayton,Dubuque,Lock and Dam 10 Control Point,SITE,US/Central,42.785,-91.095,ft,United States,IA,Clayton,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_10-CP_Clayton'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GTTI4-CP_Clayton'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_10-CP_Clayton'}, {'name': 'Agency Aliases-NIDID', 'value': 'IA04014-CP_Clayton'}]",,Mississippi River Lock and Dam 10,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate02,Hastings,Lock and Dam 02 Tainter Gate 02,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate02'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Turbine02,Hastings,Lock and Dam 02 Turbine 02,TURBINE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Turbine02'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Turbine02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Turbine02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Turbine02'}]",,Mississippi River Lock and Dam 02 Turbine 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05a-CrookedSlough,Winona,Crooked Slough near Lock5a,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-CrookedSlough'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-CrookedSlough'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-CrookedSlough'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-CrookedSlough'}]",,Mississippi River Lock and Dam 05a,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-Bays07_08,Brainerd,Pine River Dam Bays 7 & 8 - Slide Gates,SITE,US/Central,46.669117,-94.11283,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays07_08'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays07_08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays07_08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays07_08'}]",Pine River Dam Bays 7 & 8 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 7, 8" +MVP,MissHW_PineRiver-SlideGate09,Brainerd,Pine River Dam Slide Gate 09,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate09'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate09'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate09'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate09'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05a-RollerGate03,Winona,Lock and Dam 05a Roller Gate 03,OUTLET,US/Central,44.0883,-91.6699,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate03'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate03'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate03'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate03'}]",Mississippi River Lock and Dam 05a Roller Gate 03,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-Bays03_04,Brainerd,Pine River Dam Bays 3 & 4 - Slide Gates,SITE,US/Central,46.669168,-94.112654,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays03_04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays03_04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays03_04'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays03_04'}]",Pine River Dam Bays 3 & 4 - Slide Gates,Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 3, 4" +MVP,LockDam_05a-RollerGate05,Winona,Lock and Dam 05a Roller Gate 05,OUTLET,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00588-RollerGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-RollerGate05'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-RollerGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-RollerGate05'}]",Mississippi River Lock and Dam 05a Roller Gate 05,Mississippi River Lock and Dam 05a Gate,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_02-Lock_01,Hastings,Lock and Dam Lock 01,LOCK,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-Lock_01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-Lock_01'}]",,Mississippi River Lock and Dam 02,Lock,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_PineRiver-SlideGate07,Brainerd,Pine River Dam Slide Gate 07,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate07'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate07'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate07'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate07'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,MissHW_Gull-Fishway,Brainerd,Gull Fishway 01,OUTLET,US/Central,46.4116,-94.3533,m,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596-Fishway'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull-Fishway'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull-Fishway'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5-Fishway'}]",,Gull Lake Dam,,,,NAD83,335.28,NGVD29, +MVP,LockDam_02-TainterGate04,Hastings,Lock and Dam 02 Tainter Gate 04,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate04'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-Lock_01,Winona,Lock and Dam 05 Lock 01,LOCK,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-Lock_01'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-Lock_01'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-Lock_01'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-Lock_01'}]",,Mississippi River Lock and Dam 05,Lock,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02-TainterGate06,Hastings,Lock and Dam 02 Tainter Gate 06,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate06'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate06'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_05a,Fountain City,Lock and Dam 5A,PROJECT,US/Central,44.0890583,-91.6722333,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588'}]",Lock and Dam 05a at Mississippi River 9 foot Channel Navigation Project,Lock and Dam 05a,Dam,0.0,0.0,NAD83,599.9999999999999,LOCAL,USACE Owned and Maintained +MVP,LockDam_02-CP_SoStPaul,Hastings,Lock and Dam 02 Control Point,SITE,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-CP_SoStPaul'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-CP_SoStPaul'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-CP_SoStPaul'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-CP_SoStPaul'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate05,Brainerd,Pine River Dam Slide Gate 05,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate05'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate05'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate05'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate05'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,LockDam_05-RollerGate02,Winona,Lock and Dam 05 Roller Gate 02,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate02'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate02'}]",Mississippi River Lock and Dam 05 Roller Gate 02,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,MissHW_Gull,Brainerd,Gull Lake Dam,PROJECT,US/Central,46.4110944,-94.353575,ft,United States,MN,Cass,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00596'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_Gull'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'GLLM5'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_Gull'}]",Mississippi River Headwater Gull Lake Dam,Gull Lake Dam,Dam,,,NAD83,1099.9999999999998,NGVD29,USACE Owned and Maintained +MVP,LockDam_05-TainterValves,Winona,Lock and Dam 05 Tainter Valves,OUTLET,US/Central,44.1616,-91.8116,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-TainterValves'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-TainterValves'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-TainterValves'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-TainterValves'}]",Mississippi River Lock and Dam 05 Tainter Valves,Mississippi River Lock and Dam 05,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,LockDam_05-RollerGate04,Winona,Lock and Dam 05 Roller Gate 04,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate04'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate04'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate04'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate04'}]",Mississippi River Lock and Dam 05 Roller Gate 04,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,Rafferty_Dam,Williston,Rafferty Reservoir,SITE,US/Central,49.1455556,-103.0944444,m,Canada,00,Unknown County or County N/A for Unknown State or State N/A,,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'Rafferty_Dam'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'RAFQ8'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'Rafferty_Dam'}, {'name': 'Agency Aliases-ECCC Station ID', 'value': '05NB032'}]","Rafferty Reservoir near Estevan, SK",Rafferty Reservoir near Estevan,DCP Gage,,,,,,Owner is Environment Canada +MVP,LockDam_02-TainterGate08,Hastings,Lock and Dam 02 Tainter Gate 08,OUTLET,US/Central,44.7599,-92.8683,m,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate08'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate08'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate08'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate08'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-SlideGate13,Brainerd,Pine River Dam Slide Gate 13,OUTLET,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate13'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate13'}]",,Pine River Dam at Cross Lake,,,,NAD83,365.76,NGVD29, +MVP,MissHW_PineRiver-Bays11_13,Brainerd,"Pine River Dam Bays 11, 12, & 13 - Slide Gates",SITE,US/Central,46.669057,-94.113025,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Bays11_13'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Bays11_13'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Bays11_13'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Bays11_13'}]","Pine River Dam Bays 11, 12, & 13 - Slide Gates",Pine River Dam at Cross Lake,,-3.4028234663852886e+38,-3.4028234663852886e+38,NAD83,0.0,NGVD29,"Total opening of gates in bays 11, 12, 13" +MVP,LockDam_02-TainterGate10,Hastings,Lock and Dam 02 Tainter Gate 10,OUTLET,US/Central,44.7599,-92.8683,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594-TainterGate10'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5-TainterGate10'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02-TainterGate10'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02-TainterGate10'}]",,Mississippi River Lock and Dam 02,,0.0,0.0,NAD83,599.9999999999999,NAVD88, +MVP,LockDam_02,Hastings,Lock and Dam 2,PROJECT,US/Central,44.7621,-92.8712833,ft,United States,MN,Dakota,MVP,True,"[{'name': 'Agency Aliases-NIDID', 'value': 'MN00594'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_02'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_02'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'HSTM5'}]",Lock and Dam 02 at Mississippi River 9 foot ChannelNavigation Project,Lock and Dam 02,Dam,0.0,0.0,NAD83,599.9999999999999,LOCAL,"USACE Owned and Maintained, Brookfield Power operates hydropower" +MVP,LockDam_05a-TainterGates,Winona,Lock and Dam 05a Tainter Gates,SITE,US/Central,44.0883,-91.6699,m,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05a-TainterGates'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'WIDM5-TainterGates'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05a-TainterGates'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00588-TainterGates'}]",Mississippi River Lock and Dam 05a Tainter Gates,Mississippi River Lock and Dam 05a Tainter Gates,Gate,0.0,0.0,NAD83,182.88,NAVD88, +MVP,MissHW_PineRiver-Tailwater,Brainerd,Pine River Dam Tailwater,SITE,US/Central,46.6683,-94.1116,m,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-Tailwater'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-Tailwater'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-Tailwater'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-Tailwater'}]",,Pine River Dam at Cross Lake,Dam,,,NAD83,365.76,NGVD29, +MVP,MissHW_PineRiver-SlideGate11,Brainerd,Pine River Dam Slide Gate 11,OUTLET,US/Central,46.6683,-94.1116,ft,United States,MN,Crow Wing,MVP,True,"[{'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'MissHW_PineRiver-SlideGate11'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'CRLM5-SlideGate11'}, {'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'MissHW_PineRiver-SlideGate11'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00582-SlideGate11'}]",,Pine River Dam at Cross Lake,,,,NAD83,1199.9999999999998,NGVD29, +MVP,LockDam_05-RollerGate06,Winona,Lock and Dam 05 Roller Gate 06,OUTLET,US/Central,44.1616,-91.8116,ft,United States,WI,Buffalo,MVP,True,"[{'name': 'Agency Aliases-CWMS Standard Naming', 'value': 'LockDam_05-RollerGate06'}, {'name': 'Agency Aliases-CWMS Legacy Naming', 'value': 'LockDam_05-RollerGate06'}, {'name': 'Agency Aliases-NWS Handbook 5 ID', 'value': 'MSCM5-RollerGate06'}, {'name': 'Agency Aliases-NIDID', 'value': 'MN00589-RollerGate06'}]",Mississippi River Lock and Dam 05 Roller Gate 06,Mississippi River Lock and Dam 05 Gate,Gate,0.0,0.0,NAD83,599.9999999999999,NAVD88, diff --git a/load_data/data/MVP_timeseries_ids_all.csv b/load_data/data/MVP_timeseries_ids_all.csv new file mode 100755 index 0000000000..8c5cea6658 --- /dev/null +++ b/load_data/data/MVP_timeseries_ids_all.csv @@ -0,0 +1,1653 @@ +office,ts_id +mvp,LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-MainLake.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,TraverseWR_Dam-MainLake.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LeafGate.Elev.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LeafGate.Elev.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LeafGate.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LeafGate.Head.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam-LowFlow.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-LowFlow.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.comp +mvp,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Highway75_Dam-LowFlow.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Highway75_Dam-LowFlow.Precip-inc.Total.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-LowFlow.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-LowFlow.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-LowFlow.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam-LowFlow.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam-ServiceSpillway.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.0.0.Raw-CEMVP +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam-ServiceSpillway.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Highway75_Dam.Area.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,Highway75_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,Highway75_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-In.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,Highway75_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Flow-Out.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,Highway75_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Head.Inst.15Minutes.0.comp +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Highway75_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Highway75_Dam.Stage.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,Highway75_Dam.Stage.Inst.15Minutes.0.rev +mvp,Highway75_Dam.Stage.Inst.~15Minutes.0.best +mvp,Highway75_Dam.Stor.Ave.1Day.1Day.comp +mvp,Highway75_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-SlideGate01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate25.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate25.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate25.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05a-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05a-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_05-TainterGate27.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate27.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate27.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Flow.Inst.15Minutes.0.comp +mvp,ZUMM5.Flow.Inst.15Minutes.0.rev +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ZUMM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ZUMM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,ZUMM5.Stage.Inst.15Minutes.0.rev +mvp,ZUMM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ZUMM5.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver-SlideGate04.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LFKM5S.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,LFKM5S.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate02.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate01.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate03.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate05.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate06.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate07.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate08.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate09.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate10.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate11.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate12.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate13.Flow.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver-SlideGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-SlideGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_PineRiver.Area.Inst.15Minutes.0.comp +mvp,MissHW_PineRiver.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_PineRiver.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Month.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.1Week.comp +mvp,MissHW_PineRiver.Flow-In.Ave.1Day.3Days.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,MissHW_PineRiver.Flow-In.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Flow-In.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Flow-Out.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Flow-Out.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Flow.Inst.15Minutes.0.comp-gates +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.rev +mvp,MissHW_PineRiver.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,MissHW_PineRiver.Precip-inc.Total.1Day.1Day.comp +mvp,MissHW_PineRiver.Precip-inc.Total.1Hour.1Hour.comp +mvp,MissHW_PineRiver.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_PineRiver.Stage.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_PineRiver.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_PineRiver.Stage.Inst.15Minutes.0.rev +mvp,MissHW_PineRiver.Stage.Inst.~15Minutes.0.best +mvp,MissHW_PineRiver.Stor.Ave.1Day.1Day.comp +mvp,MissHW_PineRiver.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_PineRiver.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_PineRiver.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate13.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Lake.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Lake.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Lake.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.rev +mvp,MissHW_Gull-Lake.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Lake.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Lake.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.merged-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.rev +mvp,MissHW_Gull-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05-TainterGate15.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate17.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate19.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,DAWM5.Flow.Inst.15Minutes.0.Raw-MDNR +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,DAWM5.Stage.Inst.15Minutes.0.Raw-MDNR +mvp,MissHW_Gull-SlideGate05.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate21.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate21.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate21.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate13.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate32.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate32.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate32.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Flow.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate33.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate33.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate33.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-Tailwater.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:48+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:17+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:27+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:47+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:10+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.rev-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Temp-Water.Ave.1Day.1Day.merged +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.merged +mvp,Baldhill_Dam-Tailwater.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate17.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate34.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate34.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate34.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate29.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate29.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate29.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate30.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate30.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate30.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Clayton.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,Clayton.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Clayton.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Clayton.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Stage.Inst.15Minutes.0.rev +mvp,Clayton.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Clayton.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Clayton.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Clayton.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate31.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate31.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate31.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate11.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate12.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate14.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate16.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate18.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate20.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate20.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate20.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate22.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate22.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate22.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate24.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate24.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate24.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate26.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate26.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate26.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate28.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterGate28.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterGate28.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_05.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05.Elev.Inst.12Hours.0.740Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.741Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.742Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.743Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.745Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.746Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.747Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.748Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.749Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_05.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_05.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_05.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_05.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_05.Flow.Ave.1Day.1Day.merged +mvp,LockDam_05.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05.Flow.Inst.15Minutes.0.merged +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Head.Inst.15Minutes.0.comp +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Temp-Water.Ave.1Day.1Day.merged +mvp,LockDam_05.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05.Temp-Water.Inst.15Minutes.0.merged +mvp,LockDam_05.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM +mvp,LockDam_05.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a-CrookedSlough.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.rev +mvp,LockDam_05a-CrookedSlough.Temp-Water.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-CrookedSlough.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_05a-RollerGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a-Spillway.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_05a-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_05a-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_05a.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05a.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_05a.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05a.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_05a.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05a.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_05a.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_05a.Elev.Inst.12Hours.0.730Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.12Hours.0.731Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_05a.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_05a.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_05a.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_05a.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_05a.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_05a.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_05a.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_05a.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_05a.Flow.Ave.1Day.1Day.merged +mvp,LockDam_05a.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_05a.Flow.Inst.15Minutes.0.comp +mvp,LockDam_05a.Flow.Inst.15Minutes.0.merged +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Head.Inst.15Minutes.0.comp +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_05a.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_05a.Stage.Ave.1Day.1Day.comp +mvp,LockDam_05a.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_05a.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_05a.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Stage.Inst.15Minutes.0.rev +mvp,LockDam_05a.Stage.Inst.~15Minutes.0.best +mvp,LockDam_05a.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_05a.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,LockDam_05a.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_05a.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate05.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.test +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,Baldhill_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.%-Ice.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Area.Inst.15Minutes.0.comp +mvp,Baldhill_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,Baldhill_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,Baldhill_Dam.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.1Day.0.Regulating +mvp,Baldhill_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:46+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:16+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:26+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:45+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:08+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~1Day.0.Regulating +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.rev +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.comp +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.rev +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,Baldhill_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:45+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:15+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:25+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:44+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:08+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-In.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:45+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:15+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:25+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:44+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:07+00:00 +mvp,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.merged +mvp,Baldhill_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.merged +mvp,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.1Hour.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:47+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:16+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:27+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:46+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:09+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow.Inst.15Minutes.0.comp-gates +mvp,Baldhill_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:37+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:09+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:18+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:37+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:01+00:00 +mvp,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:04+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:30+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:42+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:01+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:24+00:00 +mvp,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Baldhill_Dam.Precip-inc.Total.1Day.1Day.comp +mvp,Baldhill_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,Baldhill_Dam.Stage.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam.Stor.Ave.15Minutes.2Hours.comp +mvp,Baldhill_Dam.Stor.Ave.1Day.1Day.comp +mvp,Baldhill_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,Baldhill_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,Baldhill_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,LockDam_02-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Powerhouse.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Powerhouse.Power.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Powerhouse.Power.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Rafferty_Dam-Tailwater.Flow.Inst.5Minutes.0.Raw-EnvCan +mvp,Rafferty_Dam-Tailwater.Stage.Inst.5Minutes.0.Raw-EnvCan +mvp,Cooperstown200.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Cooperstown200.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Cooperstown200.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown200.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-TainterGate.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-TainterGate.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ELZM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,ELZM5.Flow.Inst.0.0.Raw-USGS +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ELZM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ELZM5.Stage.Inst.0.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Lake.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Lake.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Lake.Stor.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Lake.Stor.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,TraverseWR_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate01.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,ChippewaDiv_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam.Area.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,TraverseWR_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,TraverseWR_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.rev +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.comp +mvp,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.rev +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,TraverseWR_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,TraverseWR_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.comp +mvp,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam.Flow-Out.Inst.1Hour.0.rev +mvp,TraverseWR_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,TraverseWR_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry +mvp,TraverseWR_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,TraverseWR_Dam.Stage.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,TraverseWR_Dam.Stage.Inst.15Minutes.0.rev +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.best +mvp,TraverseWR_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,TraverseWR_Dam.Stor.Ave.15Minutes.2Hours.comp +mvp,TraverseWR_Dam.Stor.Ave.1Day.1Day.comp +mvp,TraverseWR_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,TraverseWR_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,TraverseWR_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Cooperstown.Conc-OXYGEN.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Cooperstown.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Cooperstown.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Cooperstown.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:42+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:13+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:23+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:42+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:05+00:00 +mvp,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Flow.Ave.1Day.1Day.rev-USGS +mvp,Cooperstown.Flow.Inst.15Minutes.0.comp +mvp,Cooperstown.Flow.Inst.15Minutes.0.rev +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:24:43+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:13+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:23+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:58:42+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:06+00:00 +mvp,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:07+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:32+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:45+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:04+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:27+00:00 +mvp,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 15:25:07+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:41:31+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 13:01:44+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-12 01:59:04+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto:2025-06-11 15:04:26+00:00 +mvp,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Cooperstown.Stage.Ave.1Day.1Day.rev-USGS +mvp,Cooperstown.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Stage.Inst.15Minutes.0.corrected-comp +mvp,Cooperstown.Stage.Inst.15Minutes.0.rev +mvp,Cooperstown.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Cooperstown.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Cooperstown.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Cooperstown.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MOOM5S.Depth-SWE.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,MOOM5S.Depth-Snow.Inst.~1Week.0.CEMVP-ProjectEntry +mvp,ChippewaDiv_Dam.Area.Inst.15Minutes.0.comp +mvp,ChippewaDiv_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM +mvp,ChippewaDiv_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM +mvp,ChippewaDiv_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,ChippewaDiv_Dam.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Month.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Week.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.1Day.3Days.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,ChippewaDiv_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.merged +mvp,ChippewaDiv_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.merged +mvp,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,ChippewaDiv_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ChippewaDiv_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Stage.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.rev +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.best +mvp,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ChippewaDiv_Dam.Stor.Ave.1Day.1Day.comp +mvp,ChippewaDiv_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,ChippewaDiv_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Muscoda.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Muscoda.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Muscoda.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Flow.Ave.1Day.1Day.rev-USGS +mvp,Muscoda.Flow.Inst.15Minutes.0.comp +mvp,Muscoda.Flow.Inst.15Minutes.0.rev +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.rev +mvp,Muscoda.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Muscoda.Precip-inc.Total.1Day.1Day.comp +mvp,Muscoda.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Stage.Inst.15Minutes.0.corrected-comp +mvp,Muscoda.Stage.Inst.15Minutes.0.rev +mvp,Muscoda.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-SlideGate04.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-StopLog01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,NWUM5.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,NWUM5.Elev.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Elev.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,NWUM5.Flow.Inst.0.0.Raw-USGS +mvp,NWUM5.Flow.Inst.15Minutes.0.comp +mvp,NWUM5.Flow.Inst.15Minutes.0.rev +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,NWUM5.Stage.Inst.0.0.Raw-USGS +mvp,NWUM5.Stage.Inst.15Minutes.0.OTHER-GOES-Raw +mvp,NWUM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,NWUM5.Stage.Inst.15Minutes.0.rev +mvp,NWUM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,NWUM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,NWUM5.Volt.Inst.1Hour.0.OTHER-GOES-Raw +mvp,LockDam_02-TainterGate12.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate12.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,AGYM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,AGYM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,AGYM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_02-TainterGate16.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LFKM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,LFKM5.Flow.Inst.15Minutes.0.comp +mvp,LFKM5.Flow.Inst.15Minutes.0.rev +mvp,LFKM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,LFKM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,LFKM5.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Precip-cum.Inst.15Minutes.0.rev +mvp,LFKM5.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,LFKM5.Precip-inc.Total.1Day.1Day.comp +mvp,LFKM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,LFKM5.Stage.Inst.15Minutes.0.rev +mvp,LFKM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,LFKM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LFKM5.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LFKM5.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Flow.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,ABRN8.Stage.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.rev-USGS +mvp,LockDam_02-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02-Tailwater.Flow.Ave.1Day.1Day.merged +mvp,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.merged +mvp,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_02-Tailwater.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_02-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Fishway.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Fishway.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_Gull.Area.Inst.1Hour.0.comp +mvp,MissHW_Gull.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,MissHW_Gull.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_Gull.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,MissHW_Gull.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Elev.Inst.1Hour.0.merged-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Month.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.1Week.comp +mvp,MissHW_Gull.Flow-In.Ave.1Day.3Days.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp-noNeg +mvp,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp +mvp,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp-noNeg +mvp,MissHW_Gull.Flow-In.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Flow-In.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull.Flow-Out.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Flow-Out.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.rev +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Flow.Inst.1Hour.0.comp-gates +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.rev +mvp,MissHW_Gull.Precip-inc.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-inc.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-inc.Total.1Day.1Day.comp +mvp,MissHW_Gull.Precip-inc.Total.1Hour.1Hour.comp +mvp,MissHW_Gull.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,MissHW_Gull.Stage.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Stage.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.rev +mvp,MissHW_Gull.Stage.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,MissHW_Gull.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,MissHW_Gull.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,Rafferty_Dam.Elev.Inst.5Minutes.0.Raw-EnvCan +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,Rafferty_Dam.Stage.Inst.5Minutes.0.Raw-EnvCan +mvp,LockDam_02-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate08.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02.%-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_02.Code-OpenRiver.Inst.15Minutes.0.comp +mvp,LockDam_02.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_02.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM +mvp,LockDam_02.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_02.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS +mvp,LockDam_02.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Elev.Ave.1Day.1Day.merged-MSL1912 +mvp,LockDam_02.Elev.Inst.12Hours.0.818Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.819Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.820Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.823Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.824Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.827Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.837Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.839Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Elev.Inst.15Minutes.0.merged-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02.Elev.Inst.1Hour.0.Regulating +mvp,LockDam_02.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Flow-Out.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02.Flow-Out.Ave.1Day.1Day.comp +mvp,LockDam_02.Flow-Out.Ave.6Hours.6Hours.comp +mvp,LockDam_02.Flow-Out.Inst.15Minutes.0.rev +mvp,LockDam_02.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,LockDam_02.Flow-Out.Inst.~15Minutes.0.best +mvp,LockDam_02.Flow.Ave.1Day.1Day.merged +mvp,LockDam_02.Flow.Inst.12Hours.0.Fcst-CEMVP-CWMS:120HoursQPF +mvp,LockDam_02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02.Flow.Inst.15Minutes.0.merged +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Head.Inst.15Minutes.0.comp +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-11 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-10 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto +mvp,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF +mvp,LockDam_02.Precip.Total.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Stage.Ave.1Day.1Day.MVDhist-rev +mvp,LockDam_02.Stage.Ave.1Day.1Day.comp +mvp,LockDam_02.Stage.Ave.6Hours.6Hours.comp +mvp,LockDam_02.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS +mvp,LockDam_02.Temp-Water.Ave.1Day.1Day.merged +mvp,LockDam_02.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Temp-Water.Inst.15Minutes.0.merged +mvp,LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM +mvp,LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw diff --git a/load_data/data/MVP_timeseries_ids_used.csv b/load_data/data/MVP_timeseries_ids_used.csv new file mode 100755 index 0000000000..aa170b43f8 --- /dev/null +++ b/load_data/data/MVP_timeseries_ids_used.csv @@ -0,0 +1,294 @@ +office,ts_id +mvp,ABRN8.Flow.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,ABRN8.Stage.Ave.1Day.1Day.rev-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ABRN8.Stage.Inst.~15Minutes.0.rev-USGS +mvp,AGYM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,AGYM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,AGYM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,AGYM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.~1Day.0.Regulating +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.merged-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam.Elev.Inst.1Day.0.Regulating +mvp,Baldhill_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow.Inst.15Minutes.0.comp-gates +mvp,Baldhill_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.comp +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.rev +mvp,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.rev +mvp,Baldhill_Dam.Flow-In.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-In.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-07 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Flow-Out.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.1Hour.0.rev +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry +mvp,Baldhill_Dam.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Baldhill_Dam.Precip-inc.Total.1Day.1Day.comp +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam.Stor.Ave.6Hours.6Hours.comp +mvp,Baldhill_Dam.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-FishPondSiphon.Flow.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-FishPondSiphon.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,Baldhill_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-06 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp +mvp,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-Tailwater.Temp-Water.Ave.1Day.1Day.merged +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.merged +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.rev +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.rev +mvp,ELZM5.Flow.Ave.1Day.1Day.rev-USGS +mvp,ELZM5.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Flow.Inst.~15Minutes.0.rev-USGS +mvp,ELZM5.Flow.Inst.0.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ELZM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ELZM5.Stage.Inst.0.0.Raw-USGS +mvp,LockDam_02.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-09 01:11:00+00:00 +mvp,LockDam_02.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-Powerhouse.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Powerhouse.Power.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Powerhouse.Power.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912 +mvp,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.comp +mvp,LockDam_02-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,LockDam_02-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.rev +mvp,LockDam_02-TainterGate01.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate01.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate02.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate03.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate05.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate06.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate07.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate08.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate08.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate09.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate12.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate12.Opening-MaxAllow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate13.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate16.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate17.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.test +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.test +mvp,LockDam_02-TainterValves.Flow.Inst.15Minutes.0.comp +mvp,LockDam_02-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,LockDam_02-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Elev.Inst.1Hour.0.merged-NGVD29 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Flow-In.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Out.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.Fcst-CEMVP +mvp,MissHW_Gull.Flow-Out.Inst.1Hour.0.rev +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-08 01:11:00+00:00 +mvp,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS:2025-06-12 01:11:00+00:00 +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Precip-cum.Inst.1Hour.0.rev +mvp,MissHW_Gull.Precip-inc.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.~15Minutes.0.best +mvp,MissHW_Gull.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull.Stage.Inst.1Hour.0.rev +mvp,MissHW_Gull.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Fishway.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Fishway.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NGVD29 +mvp,MissHW_Gull-Lake.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.rev +mvp,MissHW_Gull-Lake.Stor.Ave.1Day.1Day.comp +mvp,MissHW_Gull-Lake.Stor.Ave.6Hours.6Hours.comp +mvp,MissHW_Gull-Lake.Volt.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-SlideGate01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate04.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate05.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Flow.Inst.1Hour.0.comp +mvp,MissHW_Gull-StopLog01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-StopLog01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry +mvp,MissHW_Gull-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NAVD88 +mvp,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NGVD29 +mvp,MissHW_Gull-Tailwater.Stage.Inst.~15Minutes.0.best +mvp,MissHW_Gull-Tailwater.Stage.Inst.0.0.Raw-CEMVP +mvp,MissHW_Gull-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.CEMVP-GOES-Raw +mvp,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.rev +mvp,Muscoda.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88 +mvp,Muscoda.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88 +mvp,Muscoda.Elev.Inst.15Minutes.0.rev-NAVD88 +mvp,Muscoda.Flow.Ave.1Day.1Day.rev-USGS +mvp,Muscoda.Flow.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Flow.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Flow.Inst.15Minutes.0.comp +mvp,Muscoda.Flow.Inst.15Minutes.0.rev +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Precip-cum.Inst.15Minutes.0.rev +mvp,Muscoda.Precip-inc.Total.15Minutes.15Minutes.comp +mvp,Muscoda.Precip-inc.Total.1Day.1Day.comp +mvp,Muscoda.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,Muscoda.Stage.Inst.~15Minutes.0.rev-USGS +mvp,Muscoda.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,Muscoda.Stage.Inst.15Minutes.0.corrected-comp +mvp,Muscoda.Stage.Inst.15Minutes.0.rev +mvp,Muscoda.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ZUMM5.Flow.Inst.15Minutes.0.comp +mvp,ZUMM5.Flow.Inst.15Minutes.0.rev +mvp,ZUMM5.Stage.Inst.~15Minutes.0.Raw-USGS +mvp,ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS +mvp,ZUMM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw +mvp,ZUMM5.Stage.Inst.15Minutes.0.corrected-comp +mvp,ZUMM5.Stage.Inst.15Minutes.0.rev diff --git a/load_data/data/MVP_timeseries_values_melted.parquet b/load_data/data/MVP_timeseries_values_melted.parquet new file mode 100755 index 0000000000..59bebada08 Binary files /dev/null and b/load_data/data/MVP_timeseries_values_melted.parquet differ diff --git a/load_data/dev_data_copy.ipynb b/load_data/dev_data_copy.ipynb new file mode 100644 index 0000000000..482244ec68 --- /dev/null +++ b/load_data/dev_data_copy.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "64426a8a-c361-4e80-986d-451bf08907a5", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'cwms'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 6\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01msys\u001b[39;00m\n\u001b[1;32m 5\u001b[0m sys\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39minsert(\u001b[38;5;241m0\u001b[39m, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mC:/Soft/repos/cwms-python\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m----> 6\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mcwms\u001b[39;00m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtime\u001b[39;00m\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'cwms'" + ] + } + ], + "source": [ + "#libraries\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "import sys\n", + "sys.path.insert(0, \"C:/Soft/repos/cwms-python\")\n", + "import cwms\n", + "import numpy as np\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "63849890-9155-4c2b-8a62-96aa9fbe0de4", + "metadata": {}, + "source": [ + "### Initialize system Load Location IDs" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a5d7f8c6-8b5d-44ec-95d1-1e4ea388d732", + "metadata": {}, + "outputs": [], + "source": [ + "office_id = 'LRL'\n", + "start_date = pd.to_datetime(\"3/1/2025\").tz_localize('UTC')\n", + "end_date = pd.to_datetime(\"6/13/2025\").tz_localize('UTC')\n", + "office_id_lower = office_id.lower() \n", + "apiRoot_src = f\"https://wm.{office_id_lower}.ds.usace.army.mil:8243/{office_id_lower}-data/\"\n", + "location_id_file = 'base_locations_to_grab.csv'\n", + "api = cwms.api.init_session(api_root=apiRoot_src)\n", + "grab_locs = pd.read_csv(location_id_file)" + ] + }, + { + "cell_type": "markdown", + "id": "7da9432f-c362-4d22-a981-9d8fb26cb593", + "metadata": {}, + "source": [ + "### Grab all locations and the get information for subset" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f9cc1f27-c358-49e3-b14d-751599feb8e3", + "metadata": {}, + "outputs": [], + "source": [ + "location = cwms.get_locations_catalog(office_id=office_id).df\n", + "grab_locs_office = grab_locs[grab_locs['Office']==office_id]\n", + "pattern = \"|\".join(grab_locs_office['Base_Location'])\n", + "public_locations = location[location['name'].str.contains(pattern)]\n", + "public_locations.to_csv(f'data/{office_id}_locations_data.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "962aac10-218b-4cab-af88-013eab522cc0", + "metadata": {}, + "source": [ + "### Collect All Timeseries ID assigned to the Locations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43985fba-3cf4-4e71-bc7b-efd997e4c18c", + "metadata": {}, + "outputs": [], + "source": [ + "final_tsids =[]\n", + "for loc in public_locations['name']:\n", + " loc_repex = loc+'.*'\n", + " cat_ts = cwms.get_timeseries_catalog(office_id=office_id,like=loc_repex,include_extents=True,timeseries_group_like=None).df\n", + " for index, ts in cat_ts.iterrows():\n", + "\n", + " extents = pd.DataFrame(ts['extents'])\n", + " if 'latest-time' in extents.columns:\n", + " if len(extents) == 1 and (pd.to_datetime(extents['latest-time'].iloc[0]) > start_date):\n", + "\n", + " final_tsids.append(ts['name'])\n", + " if len(extents) > 1:\n", + "\n", + " extents['version-time'] = pd.to_datetime(extents['version-time'])\n", + " extents = extents[extents['version-time'] > start_date]\n", + " if len(extents) > 0:\n", + " num = min([len(extents),5])\n", + " extents_sorted = extents.sort_values(by='version-time',ascending=False)\n", + " extents_tosave = extents_sorted.iloc[0:num]\n", + " for version in extents_tosave['version-time']:\n", + " final_tsids.append(f'{ts[\"name\"]}:{version}')\n", + "ts_ids = pd.DataFrame({'office':office_id,'ts_id':final_tsids}) \n", + "ts_ids = ts_ids.drop_duplicates()\n", + "ts_ids.to_csv(f'data/{office_id}_timeseries_ids.csv')" + ] + }, + { + "cell_type": "markdown", + "id": "b3b85b3f-89f1-4987-a890-0890e9fa7b2f", + "metadata": {}, + "source": [ + "### Grab Timeseries Values for All Timeseries IDS" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "ecae8f73-3ab7-4f04-9226-4325e72ad60a", + "metadata": {}, + "outputs": [], + "source": [ + "ts_ids = pd.read_csv(f'data/{office_id}_timeseries_ids.csv')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "46671d37-b440-4219-9f78-9d4042d95ffd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 59.33675069999998 seconds\n" + ] + } + ], + "source": [ + "start_time = time.perf_counter()\n", + "multi_ts_melt = cwms.get_multi_timeseries_df(ts_ids=ts_ids['ts_id'],office_id=office_id,melted=True,begin=start_date,end=end_date)\n", + "end_time = time.perf_counter()\n", + "elapsed_time = end_time - start_time\n", + "print(f\"Elapsed time: {elapsed_time} seconds\")\n", + "multi_ts_melt.to_parquet(f'data/{office_id}_timeseries_values_melted.parquet')" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "29caaf2f-842e-40d3-9f37-5095512cdd0e", + "metadata": {}, + "outputs": [], + "source": [ + "multi_ts_melt = pd.read_parquet('data/MVP_timeseries_values_melted.parquet')\n", + "apiRoot_dev = \"https://water.dev.cwbi.us/cwms-data/\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "163df7d8-6e6a-4612-a022-6af40427f6a8", + "metadata": {}, + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + " ········\n" + ] + } + ], + "source": [ + "from getpass import getpass\n", + "apiKey = getpass()\n", + "apiKey_dev = \"apikey \" + apiKey" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5a2cef41-20e3-45bd-9de2-4887727b9531", + "metadata": {}, + "outputs": [], + "source": [ + "api = cwms.api.init_session(api_root=apiRoot_dev, api_key=apiKey_dev)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d2cf42da-f5a0-4915-8a9d-4e139d6a1705", + "metadata": {}, + "outputs": [], + "source": [ + "cwms.store_multi_timeseries_df(ts_data=multi_ts_melt,office_id='MVP')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "275cdfd7-4f60-4731-bf1f-24f4129876f0", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'start_date' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[10], line 3\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mtime\u001b[39;00m\n\u001b[0;32m 2\u001b[0m start_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mperf_counter()\n\u001b[1;32m----> 3\u001b[0m multi_ts_melt_dev \u001b[38;5;241m=\u001b[39m cwms\u001b[38;5;241m.\u001b[39mget_multi_timeseries_df(ts_ids\u001b[38;5;241m=\u001b[39mfinal_tsids,office_id\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mMVP\u001b[39m\u001b[38;5;124m'\u001b[39m,melted\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m,begin\u001b[38;5;241m=\u001b[39m\u001b[43mstart_date\u001b[49m,end\u001b[38;5;241m=\u001b[39mend_date)\n\u001b[0;32m 4\u001b[0m end_time \u001b[38;5;241m=\u001b[39m time\u001b[38;5;241m.\u001b[39mperf_counter()\n\u001b[0;32m 5\u001b[0m elapsed_time \u001b[38;5;241m=\u001b[39m end_time \u001b[38;5;241m-\u001b[39m start_time\n", + "\u001b[1;31mNameError\u001b[0m: name 'start_date' is not defined" + ] + } + ], + "source": [ + "import time\n", + "start_time = time.perf_counter()\n", + "multi_ts_melt_dev = cwms.get_multi_timeseries_df(ts_ids=final_tsids,office_id='MVP',melted=True,begin=start_date,end=end_date)\n", + "end_time = time.perf_counter()\n", + "elapsed_time = end_time - start_time\n", + "print(f\"Elapsed time: {elapsed_time} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4dd0de1-9413-41e3-a956-b652865de0ea", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d408b30-61f1-4e29-9ef4-56be7298431d", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "584e2089-990c-4387-969a-a0652bb4b681", + "metadata": {}, + "outputs": [], + "source": [ + "unique_tsids = (multi_ts_melt['ts_id'].astype(str) + ':' + multi_ts_melt['version_date'].astype(str)).unique()" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "3ab1f866-ce7f-4a7b-be11-d2351abe4830", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp:NaT',\n", + " 'LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry:NaT',\n", + " 'LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry:NaT',\n", + " ..., 'LockDam_02.Temp-Water.Inst.15Minutes.0.merged:NaT',\n", + " 'LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM:NaT',\n", + " 'LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw:NaT'], dtype=object)" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unique_tsids" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "fcfeb20c-7f10-4847-af78-1fd56d99e40a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cfs'" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data['units'].iloc[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "434965dc-caf8-44ba-a266-e8ca609bcfe4", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed5b113d-e63a-4a8a-a808-521dd7a09805", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d26f19f-1319-477c-852b-363f158980c7", + "metadata": {}, + "outputs": [], + "source": [ + "data = multi_ts_melt.query('ts_id' == 'LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp' and 'version_date' < 9')" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "4f5afd19-4240-4818-bf2d-68f2119a2d6a", + "metadata": {}, + "outputs": [ + { + "ename": "ValueError", + "evalue": "Index contains duplicate entries, cannot reshape", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[45], line 11\u001b[0m\n\u001b[0;32m 7\u001b[0m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m=\u001b[39m (\n\u001b[0;32m 8\u001b[0m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mstr[:\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m2\u001b[39m] \u001b[38;5;241m+\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m:\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m multi_ts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m]\u001b[38;5;241m.\u001b[39mstr[\u001b[38;5;241m-\u001b[39m\u001b[38;5;241m2\u001b[39m:]\n\u001b[0;32m 9\u001b[0m )\n\u001b[0;32m 10\u001b[0m multi_ts\u001b[38;5;241m.\u001b[39mfillna({\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mversion_date\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m}, inplace\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m---> 11\u001b[0m multi_ts_unmelt \u001b[38;5;241m=\u001b[39m \u001b[43mmulti_ts\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mdate-time\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcols\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalues\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mvalue\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\frame.py:9025\u001b[0m, in \u001b[0;36mDataFrame.pivot\u001b[1;34m(self, columns, index, values)\u001b[0m\n\u001b[0;32m 9018\u001b[0m \u001b[38;5;129m@Substitution\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 9019\u001b[0m \u001b[38;5;129m@Appender\u001b[39m(_shared_docs[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpivot\u001b[39m\u001b[38;5;124m\"\u001b[39m])\n\u001b[0;32m 9020\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mpivot\u001b[39m(\n\u001b[0;32m 9021\u001b[0m \u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39m, columns, index\u001b[38;5;241m=\u001b[39mlib\u001b[38;5;241m.\u001b[39mno_default, values\u001b[38;5;241m=\u001b[39mlib\u001b[38;5;241m.\u001b[39mno_default\n\u001b[0;32m 9022\u001b[0m ) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m DataFrame:\n\u001b[0;32m 9023\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpivot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m pivot\n\u001b[1;32m-> 9025\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mpivot\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mindex\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcolumns\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcolumns\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mvalues\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\pivot.py:553\u001b[0m, in \u001b[0;36mpivot\u001b[1;34m(data, columns, index, values)\u001b[0m\n\u001b[0;32m 549\u001b[0m indexed \u001b[38;5;241m=\u001b[39m data\u001b[38;5;241m.\u001b[39m_constructor_sliced(data[values]\u001b[38;5;241m.\u001b[39m_values, index\u001b[38;5;241m=\u001b[39mmultiindex)\n\u001b[0;32m 550\u001b[0m \u001b[38;5;66;03m# error: Argument 1 to \"unstack\" of \"DataFrame\" has incompatible type \"Union\u001b[39;00m\n\u001b[0;32m 551\u001b[0m \u001b[38;5;66;03m# [List[Any], ExtensionArray, ndarray[Any, Any], Index, Series]\"; expected\u001b[39;00m\n\u001b[0;32m 552\u001b[0m \u001b[38;5;66;03m# \"Hashable\"\u001b[39;00m\n\u001b[1;32m--> 553\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[43mindexed\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcolumns_listlike\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n\u001b[0;32m 554\u001b[0m result\u001b[38;5;241m.\u001b[39mindex\u001b[38;5;241m.\u001b[39mnames \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m 555\u001b[0m name \u001b[38;5;28;01mif\u001b[39;00m name \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m lib\u001b[38;5;241m.\u001b[39mno_default \u001b[38;5;28;01melse\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;28;01mfor\u001b[39;00m name \u001b[38;5;129;01min\u001b[39;00m result\u001b[38;5;241m.\u001b[39mindex\u001b[38;5;241m.\u001b[39mnames\n\u001b[0;32m 556\u001b[0m ]\n\u001b[0;32m 558\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\series.py:4455\u001b[0m, in \u001b[0;36mSeries.unstack\u001b[1;34m(self, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 4410\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4411\u001b[0m \u001b[38;5;124;03mUnstack, also known as pivot, Series with MultiIndex to produce DataFrame.\u001b[39;00m\n\u001b[0;32m 4412\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 4451\u001b[0m \u001b[38;5;124;03mb 2 4\u001b[39;00m\n\u001b[0;32m 4452\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4453\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m unstack\n\u001b[1;32m-> 4455\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:494\u001b[0m, in \u001b[0;36munstack\u001b[1;34m(obj, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 490\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(level, (\u001b[38;5;28mtuple\u001b[39m, \u001b[38;5;28mlist\u001b[39m)):\n\u001b[0;32m 491\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(level) \u001b[38;5;241m!=\u001b[39m \u001b[38;5;241m1\u001b[39m:\n\u001b[0;32m 492\u001b[0m \u001b[38;5;66;03m# _unstack_multiple only handles MultiIndexes,\u001b[39;00m\n\u001b[0;32m 493\u001b[0m \u001b[38;5;66;03m# and isn't needed for a single level\u001b[39;00m\n\u001b[1;32m--> 494\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_unstack_multiple\u001b[49m\u001b[43m(\u001b[49m\u001b[43mobj\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 495\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m 496\u001b[0m level \u001b[38;5;241m=\u001b[39m level[\u001b[38;5;241m0\u001b[39m]\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:445\u001b[0m, in \u001b[0;36m_unstack_multiple\u001b[1;34m(data, clocs, fill_value, sort)\u001b[0m\n\u001b[0;32m 442\u001b[0m dummy \u001b[38;5;241m=\u001b[39m data\u001b[38;5;241m.\u001b[39mcopy()\n\u001b[0;32m 443\u001b[0m dummy\u001b[38;5;241m.\u001b[39mindex \u001b[38;5;241m=\u001b[39m dummy_index\n\u001b[1;32m--> 445\u001b[0m unstacked \u001b[38;5;241m=\u001b[39m \u001b[43mdummy\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m__placeholder__\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 446\u001b[0m new_levels \u001b[38;5;241m=\u001b[39m clevels\n\u001b[0;32m 447\u001b[0m new_names \u001b[38;5;241m=\u001b[39m cnames\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\series.py:4455\u001b[0m, in \u001b[0;36mSeries.unstack\u001b[1;34m(self, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 4410\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4411\u001b[0m \u001b[38;5;124;03mUnstack, also known as pivot, Series with MultiIndex to produce DataFrame.\u001b[39;00m\n\u001b[0;32m 4412\u001b[0m \n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 4451\u001b[0m \u001b[38;5;124;03mb 2 4\u001b[39;00m\n\u001b[0;32m 4452\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[0;32m 4453\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcore\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreshape\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m unstack\n\u001b[1;32m-> 4455\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43munstack\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfill_value\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:517\u001b[0m, in \u001b[0;36munstack\u001b[1;34m(obj, level, fill_value, sort)\u001b[0m\n\u001b[0;32m 515\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m is_1d_only_ea_dtype(obj\u001b[38;5;241m.\u001b[39mdtype):\n\u001b[0;32m 516\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _unstack_extension_series(obj, level, fill_value, sort\u001b[38;5;241m=\u001b[39msort)\n\u001b[1;32m--> 517\u001b[0m unstacker \u001b[38;5;241m=\u001b[39m \u001b[43m_Unstacker\u001b[49m\u001b[43m(\u001b[49m\n\u001b[0;32m 518\u001b[0m \u001b[43m \u001b[49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mindex\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlevel\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlevel\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconstructor\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mobj\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_constructor_expanddim\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msort\u001b[49m\n\u001b[0;32m 519\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[0;32m 520\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m unstacker\u001b[38;5;241m.\u001b[39mget_result(\n\u001b[0;32m 521\u001b[0m obj\u001b[38;5;241m.\u001b[39m_values, value_columns\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, fill_value\u001b[38;5;241m=\u001b[39mfill_value\n\u001b[0;32m 522\u001b[0m )\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:154\u001b[0m, in \u001b[0;36m_Unstacker.__init__\u001b[1;34m(self, index, level, constructor, sort)\u001b[0m\n\u001b[0;32m 146\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m num_cells \u001b[38;5;241m>\u001b[39m np\u001b[38;5;241m.\u001b[39miinfo(np\u001b[38;5;241m.\u001b[39mint32)\u001b[38;5;241m.\u001b[39mmax:\n\u001b[0;32m 147\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[0;32m 148\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThe following operation may generate \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnum_cells\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m cells \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 149\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124min the resulting pandas object.\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 150\u001b[0m PerformanceWarning,\n\u001b[0;32m 151\u001b[0m stacklevel\u001b[38;5;241m=\u001b[39mfind_stack_level(),\n\u001b[0;32m 152\u001b[0m )\n\u001b[1;32m--> 154\u001b[0m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_make_selectors\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[1;32mC:\\Soft\\Anaconda3\\envs\\env-cwms\\lib\\site-packages\\pandas\\core\\reshape\\reshape.py:210\u001b[0m, in \u001b[0;36m_Unstacker._make_selectors\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 207\u001b[0m mask\u001b[38;5;241m.\u001b[39mput(selector, \u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m 209\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m mask\u001b[38;5;241m.\u001b[39msum() \u001b[38;5;241m<\u001b[39m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mindex):\n\u001b[1;32m--> 210\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIndex contains duplicate entries, cannot reshape\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 212\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mgroup_index \u001b[38;5;241m=\u001b[39m comp_index\n\u001b[0;32m 213\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mmask \u001b[38;5;241m=\u001b[39m mask\n", + "\u001b[1;31mValueError\u001b[0m: Index contains duplicate entries, cannot reshape" + ] + } + ], + "source": [ + "cols = [\"ts_id\", \"units\"]\n", + "if \"version_date\" in multi_ts.columns:\n", + " cols.append(\"version_date\")\n", + " multi_ts[\"version_date\"] = multi_ts[\"version_date\"].dt.strftime(\n", + " \"%Y-%m-%d %H:%M:%S%z\"\n", + " )\n", + " multi_ts[\"version_date\"] = (\n", + " multi_ts[\"version_date\"].str[:-2] + \":\" + multi_ts[\"version_date\"].str[-2:]\n", + " )\n", + " multi_ts.fillna({\"version_date\": \"\"}, inplace=True)\n", + "multi_ts_unmelt = multi_ts.pivot(index=\"date-time\", columns=cols, values=\"value\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b4c37c8-7a23-466e-804b-1ace3757e68c", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da6a9982-318b-46a0-80b1-3db1c5d51235", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a88d03ff-d144-4219-847c-8c65fbe14ed1", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3e4e1368-a2c4-4483-ac2c-695110e8a02e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
date-timevaluequality-codets_idunits
02025-03-01 00:00:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
12025-03-01 00:15:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
22025-03-01 00:30:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
32025-03-01 00:45:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
42025-03-01 01:00:00+00:000.00LockDam_05-TainterGate23.Opening.Inst.15Minute...ft
..................
41293502025-06-06 10:30:00+00:0069.40LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293512025-06-07 10:00:00+00:0069.30LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293522025-06-09 10:00:00+00:0067.00LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293532025-06-11 10:00:00+00:0068.80LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
41293542025-06-12 10:00:00+00:0067.60LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEMF
\n", + "

4129355 rows × 5 columns

\n", + "
" + ], + "text/plain": [ + " date-time value quality-code \\\n", + "0 2025-03-01 00:00:00+00:00 0.0 0 \n", + "1 2025-03-01 00:15:00+00:00 0.0 0 \n", + "2 2025-03-01 00:30:00+00:00 0.0 0 \n", + "3 2025-03-01 00:45:00+00:00 0.0 0 \n", + "4 2025-03-01 01:00:00+00:00 0.0 0 \n", + "... ... ... ... \n", + "4129350 2025-06-06 10:30:00+00:00 69.4 0 \n", + "4129351 2025-06-07 10:00:00+00:00 69.3 0 \n", + "4129352 2025-06-09 10:00:00+00:00 67.0 0 \n", + "4129353 2025-06-11 10:00:00+00:00 68.8 0 \n", + "4129354 2025-06-12 10:00:00+00:00 67.6 0 \n", + "\n", + " ts_id units \n", + "0 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "1 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "2 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "3 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "4 LockDam_05-TainterGate23.Opening.Inst.15Minute... ft \n", + "... ... ... \n", + "4129350 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129351 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129352 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129353 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "4129354 LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM F \n", + "\n", + "[4129355 rows x 5 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "\n", + "multi_ts_melt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fcf78f9-5007-4440-bd4e-0b08fda5f812", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/load_data/dev_data_load.ipynb b/load_data/dev_data_load.ipynb new file mode 100644 index 0000000000..7837dcb364 --- /dev/null +++ b/load_data/dev_data_load.ipynb @@ -0,0 +1,279 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "fc24b3e1-2a70-436a-a8e3-0bf0e8a9195f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "import sys\n", + "import cwms\n", + "import numpy as np\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "978352f0-352e-4303-ae23-93838a17e704", + "metadata": {}, + "source": [ + "### Initialize CDA root and key" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb32f1d3-62a1-447a-b7fe-d0b48f3ccf01", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "office_ids = ['LRL','MVP']\n", + "apiRoot_dev = \"http://localhost:8081/cwms-data/\"\n", + "apiKey_dev = \"apikey testkey12345677\"\n", + "api = cwms.api.init_session(api_root=apiRoot_dev, api_key=apiKey_dev)" + ] + }, + { + "cell_type": "markdown", + "id": "7bd62514-550c-4f3f-a907-52502b10d3ff", + "metadata": {}, + "source": [ + "### Store Locations to Database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04001bfd-ef62-4233-a249-cb9cbcba1042", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def default_val(value, default):\n", + " if pd.isna(value) or value == 'Unknown or Not Applicable':\n", + " value = default\n", + " return value\n", + "\n", + "def store_multi_location_df(locations):\n", + " for i,row in locations.iterrows():\n", + " if row['active']:\n", + " loc_json = {\n", + " \"office-id\": row['office'], # required\n", + " \"name\": row['name'], #required\n", + " \"latitude\": float(default_val(row['latitude'],'38.0')), #required\n", + " \"longitude\": float(default_val(row['longitude'],'-85.0')), #required\n", + " \"active\": row['active'], #required\n", + " \"public-name\": default_val(row['public-name'],row['name']),\n", + " \"long-name\": row['long-name'],\n", + " \"description\": row['description'],\n", + " \"timezone-name\": default_val(row['time-zone'],'US/Eastern'), #required\n", + " \"location-type\": row['type'], \n", + " \"location-kind\": row['kind'], #required\n", + " \"nation\": 'US', #required and abbreviated\n", + " #\"state-initial\": row['state'], #Saving state doesn't work.\n", + " #\"county-name\": row['county'],\n", + " \"nearest-city\": row['nearest-city'],\n", + " \"horizontal-datum\": default_val(row['horizontal-datum'],'NAD27'), #required\n", + " #\"published-longitude\": float(row['published-longitude']),\n", + " #\"published-latitude\": float(row['published-latitude']),\n", + " \"vertical-datum\": row['vertical-datum'],\n", + " \"elevation\": float(row['elevation']),\n", + " \"map-label\": row['map-label'],\n", + " \"bounding-office-id\": row['bounding-office'],\n", + " \"elevation-units\": row['unit']\n", + " }\n", + " clean_dict = {k: loc_json[k] for k in loc_json if not pd.isna(loc_json[k])}\n", + " #try:\n", + " cwms.store_location(data = clean_dict)\n", + " #except:\n", + " # print(clean_dict)\n", + " # print('save failed')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6c56107-9c10-4c52-b8b3-2bf91e1d01ea", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for office_id in office_ids:\n", + " locations = pd.read_csv(f'data/{office_id}_locations_data.csv')\n", + " store_multi_location_df(locations)" + ] + }, + { + "cell_type": "markdown", + "id": "ec280dcb-a4e3-4a58-af91-c16aa1d88441", + "metadata": {}, + "source": [ + "#### Check if Locations are present" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48b25256-f248-4247-ae03-f86e3b961584", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check = cwms.get_locations_catalog(office_id='LRL').df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9130c6f-b28c-4fba-be63-d9500787664b", + "metadata": {}, + "outputs": [], + "source": [ + "location_check" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b1c2c8a-ff32-4087-a690-6ae32c0ff8f1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check = cwms.get_locations_catalog(office_id='MVP').df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41aac268-fbec-490f-aaa1-471d975fcd43", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "location_check" + ] + }, + { + "cell_type": "markdown", + "id": "105169d5-4216-4429-8ffc-a356a0aa99a9", + "metadata": {}, + "source": [ + "### Store Timeseries Values To Database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e3e0a05-ef0e-450d-8b10-d5f8a8948463", + "metadata": {}, + "outputs": [], + "source": [ + "for office_id in office_ids:\n", + " print(f'importing timeseries for {office_id}')\n", + " ts_ids = pd.read_csv(f'data/{office_id}_timeseries_ids_used.csv')\n", + " multi_ts_melt = pd.read_parquet(f'data/{office_id}_timeseries_values_melted.parquet')\n", + " multi_ts_melt = multi_ts_melt.dropna(subset=['value'])\n", + " multi_ts_melt_used = multi_ts_melt[multi_ts_melt['ts_id'].isin(ts_ids['ts_id'])]\n", + " cwms.store_multi_timeseries_df(data=multi_ts_melt_used,office_id=office_id,max_workers=10)" + ] + }, + { + "cell_type": "markdown", + "id": "631f8aee-aa70-41cf-bfaf-d75f36b7d95b", + "metadata": {}, + "source": [ + "#### Check Timeseries Values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d0da1e2-9b1c-4655-aa8f-b377181b4508", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ts_ids = cwms.get_timeseries_catalog(office_id='MVP',timeseries_group_like=None,page_size=10000,include_extents=True).df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3b50dd7-c0dd-4d1c-8a94-9adf904f93b0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "ts_ids" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcf0c940-97e9-48e9-9fd1-e73d92b86c0f", + "metadata": {}, + "outputs": [], + "source": [ + "start_date = pd.to_datetime('03/01/2025').tz_localize('UTC')\n", + "end_date = pd.to_datetime('06/13/2025').tz_localize('UTC')\n", + "data = cwms.get_timeseries(ts_id='ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS',office_id='MVP',begin=start_date,end=end_date)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57cdb085-a27a-4814-b834-827728474f0e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data.df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8926cbfa-6029-415f-bc13-29c9c4a3bb21", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/load_data/test.csv b/load_data/test.csv new file mode 100644 index 0000000000..8734fe9efe --- /dev/null +++ b/load_data/test.csv @@ -0,0 +1,1241 @@ +,office,name,units,interval,interval-offset,time-zone,extents +0,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:16.063259Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:17.081731Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:16.0351Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:15.841988Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:14.655117Z'}]" +1,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:17.12Z'}]" +2,MVP,ABRN8.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:16.174Z'}]" +3,MVP,ABRN8.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-22T05:00:00Z', 'last-update': '2025-06-16T18:43:17.214247Z'}]" +4,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:18.586Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:18.351288Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:19.609036Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:18.338475Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:17.268529Z'}]" +5,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:18.511Z'}]" +6,MVP,ABRN8.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:18.689Z'}]" +7,MVP,ABRN8.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-09T03:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:19.633Z'}]" +8,MVP,ABRN8.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-09T03:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:19.73Z'}]" +9,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:22.379599Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:20.954053Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:20.915176Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:22.192557Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:19.884108Z'}]" +10,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:20.994Z'}]" +11,MVP,ABRN8.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:21.256Z'}]" +12,MVP,ABRN8.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-22T05:00:00Z', 'last-update': '2025-06-16T18:43:22.302515Z'}]" +13,MVP,ABRN8.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.534Z'}]" +14,MVP,ABRN8.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.433Z'}]" +15,MVP,AGYM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-04-11T05:00:00Z', 'latest-time': '2025-05-02T05:00:00Z', 'last-update': '2025-06-16T18:42:48.29Z'}]" +16,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:42:52.946995Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:51.942805Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:51.891592Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:49.521024Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:49.366422Z'}]" +17,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:52.939Z'}]" +18,MVP,AGYM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:42:53.339Z'}]" +19,MVP,AGYM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-15T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:54.693Z'}]" +20,MVP,AGYM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-15T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:54.495Z'}]" +21,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:42:57.23343Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:57.221488Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:59.316883Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:58.074952Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:56.804937Z'}]" +22,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.183Z'}]" +23,MVP,AGYM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:42:58.231Z'}]" +24,MVP,AGYM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.505Z'}]" +25,MVP,AGYM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-03T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:58.42Z'}]" +26,MVP,Baldhill_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:01.697Z'}]" +27,MVP,Baldhill_Dam-FishPondSiphon.Flow.Inst.15Minutes.0.CEMVP-ProjectEntry,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:41.038Z'}]" +28,MVP,Baldhill_Dam-FishPondSiphon.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:40.959Z'}]" +29,MVP,Baldhill_Dam-FishPondSiphon.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:35:40.141Z'}]" +30,MVP,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:08.25Z'}]" +31,MVP,Baldhill_Dam-LowFlow01.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:01.942Z'}]" +32,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:48.947Z'}]" +33,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:05.728Z'}]" +34,MVP,Baldhill_Dam-LowFlow01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:39:06.846Z'}]" +35,MVP,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:09.364Z'}]" +36,MVP,Baldhill_Dam-LowFlow02.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:09.37Z'}]" +37,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:04.069762Z'}]" +38,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:10.845Z'}]" +39,MVP,Baldhill_Dam-LowFlow02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:17:06.967Z'}]" +40,MVP,Baldhill_Dam-Tailwater.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw,umho/cm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:51.209Z'}]" +41,MVP,Baldhill_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:52.704Z'}]" +42,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:52.396Z'}]" +43,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:51.445Z'}]" +44,MVP,Baldhill_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:54.956Z'}]" +45,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:55.039Z'}]" +46,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:56.491Z'}]" +47,MVP,Baldhill_Dam-Tailwater.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:56.542Z'}]" +48,MVP,Baldhill_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T17:57:48.426Z'}]" +49,MVP,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:57:51.998Z'}]" +50,MVP,Baldhill_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:57:52.095Z'}]" +51,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:35:57.662861Z'}]" +52,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:10Z', 'last-update': '2025-06-16T18:35:59.250667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:47Z', 'last-update': '2025-06-16T18:35:59.141927Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:27Z', 'last-update': '2025-06-16T18:36:00.397044Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:17Z', 'last-update': '2025-06-16T18:36:00.236065Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:48Z', 'last-update': '2025-06-16T18:35:58.876053Z'}]" +53,MVP,Baldhill_Dam-Tailwater.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:36:00.232Z'}]" +54,MVP,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:00.447Z'}]" +55,MVP,Baldhill_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:01.348Z'}]" +56,MVP,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:36:01.436Z'}]" +57,MVP,Baldhill_Dam-Tailwater.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T18:36:01.531Z'}]" +58,MVP,Baldhill_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:01.645Z'}]" +59,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:03.961Z'}]" +60,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:02.96Z'}]" +61,MVP,Baldhill_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.244Z'}]" +62,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.344Z'}]" +63,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:06.45Z'}]" +64,MVP,Baldhill_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-22T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:04.243Z'}]" +65,MVP,Baldhill_Dam-Tailwater.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:36:05.569Z'}]" +66,MVP,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-11T15:00:00Z', 'last-update': '2025-06-16T18:36:06.648Z'}]" +67,MVP,Baldhill_Dam-Tailwater.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-11T15:00:00Z', 'last-update': '2025-06-16T18:36:07.827Z'}]" +68,MVP,Baldhill_Dam-Tailwater.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:10.485Z'}]" +69,MVP,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:11.106Z'}]" +70,MVP,Baldhill_Dam-TainterGate01.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:19.982796Z'}]" +71,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:12.236Z'}]" +72,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:13.54Z'}]" +73,MVP,Baldhill_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T17:33:01.820815Z'}]" +74,MVP,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.3Z'}]" +75,MVP,Baldhill_Dam-TainterGate02.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:34.587Z'}]" +76,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:30.416Z'}]" +77,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:30.141Z'}]" +78,MVP,Baldhill_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T17:58:19.802Z'}]" +79,MVP,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:27.234165Z'}]" +80,MVP,Baldhill_Dam-TainterGate03.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:28.520759Z'}]" +81,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:18.446Z'}]" +82,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:20.797Z'}]" +83,MVP,Baldhill_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-08T12:45:00Z', 'last-update': '2025-06-16T18:39:19.539Z'}]" +84,MVP,Baldhill_Dam.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.025Z'}]" +85,MVP,Baldhill_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:19.491887Z'}]" +86,MVP,Baldhill_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.308Z'}]" +87,MVP,Baldhill_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.428Z'}]" +88,MVP,Baldhill_Dam.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-12T13:00:00Z', 'last-update': '2025-06-16T18:17:08.428Z'}]" +89,MVP,Baldhill_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:21.191Z'}]" +90,MVP,Baldhill_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:21.277Z'}]" +91,MVP,Baldhill_Dam.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:22.424Z'}]" +92,MVP,Baldhill_Dam.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:22.346Z'}]" +93,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:23.454Z'}]" +94,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:23.852Z'}]" +95,MVP,Baldhill_Dam.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:24.675Z'}]" +96,MVP,Baldhill_Dam.Elev.Inst.1Day.0.Regulating,m,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:24.961Z'}]" +97,MVP,Baldhill_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:25.052Z'}]" +98,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:28.720575Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:28.672197Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:28.572603Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:27.444902Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:27.171473Z'}]" +99,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:08Z', 'last-update': '2025-06-16T18:39:32.500789Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:45Z', 'last-update': '2025-06-16T18:39:30.115722Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:26Z', 'last-update': '2025-06-16T18:39:32.710233Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:16Z', 'last-update': '2025-06-16T18:39:32.316786Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:46Z', 'last-update': '2025-06-16T18:39:29.873925Z'}]" +100,MVP,Baldhill_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:29.965Z'}]" +101,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.Raw-USGS-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:30.957Z'}]" +102,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:31.072Z'}]" +103,MVP,Baldhill_Dam.Elev.Inst.~15Minutes.0.rev-USGS-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:31.373Z'}]" +104,MVP,Baldhill_Dam.Elev.Inst.~1Day.0.Regulating,m,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T06:00:00Z', 'latest-time': '2025-04-16T06:00:00Z', 'last-update': '2025-06-16T18:39:31.061Z'}]" +105,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:33.501Z'}]" +106,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.1Day.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:32.577Z'}]" +107,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:34.773Z'}]" +108,MVP,Baldhill_Dam.Flow-In.Ave.15Minutes.6Hours.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:33.751Z'}]" +109,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:33.9Z'}]" +110,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:39:33.97Z'}]" +111,MVP,Baldhill_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:39:35.06Z'}]" +112,MVP,Baldhill_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:39:35.164Z'}]" +113,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.093Z'}]" +114,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:39.887Z'}]" +115,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:35.068Z'}]" +116,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.455Z'}]" +117,MVP,Baldhill_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:37.459Z'}]" +118,MVP,Baldhill_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:36.465Z'}]" +119,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:38.930637Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:38.877042Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:41.318813Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:38.593465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:41.105721Z'}]" +120,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:08Z', 'last-update': '2025-06-16T18:39:41.102657Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:44Z', 'last-update': '2025-06-16T18:39:41.165453Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:25Z', 'last-update': '2025-06-16T18:39:40.174691Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:15Z', 'last-update': '2025-06-16T18:39:40.198756Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:45Z', 'last-update': '2025-06-16T18:39:39.884536Z'}]" +121,MVP,Baldhill_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:41.204Z'}]" +122,MVP,Baldhill_Dam.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:42.37Z'}]" +123,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:42.619833Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:43.645438Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:43.795427Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:43.961306Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:42.41772Z'}]" +124,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:07Z', 'last-update': '2025-06-16T18:39:45.160249Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:44Z', 'last-update': '2025-06-16T18:39:43.820706Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:25Z', 'last-update': '2025-06-16T18:39:43.667113Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:15Z', 'last-update': '2025-06-16T18:39:44.940015Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:45Z', 'last-update': '2025-06-16T18:39:42.675927Z'}]" +125,MVP,Baldhill_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:43.644Z'}]" +126,MVP,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:43.668Z'}]" +127,MVP,Baldhill_Dam.Flow-Out.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:43.799Z'}]" +128,MVP,Baldhill_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.789Z'}]" +129,MVP,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.857Z'}]" +130,MVP,Baldhill_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.927Z'}]" +131,MVP,Baldhill_Dam.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:43.969Z'}]" +132,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:46.385138Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:46.228256Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:45.216667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:47.476895Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:44.990174Z'}]" +133,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:09Z', 'last-update': '2025-06-16T18:39:46.43455Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:46Z', 'last-update': '2025-06-16T18:39:46.415155Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:27Z', 'last-update': '2025-06-16T18:39:46.38542Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:16Z', 'last-update': '2025-06-16T18:39:48.765305Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:47Z', 'last-update': '2025-06-16T18:39:46.245949Z'}]" +134,MVP,Baldhill_Dam.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:46.539Z'}]" +135,MVP,Baldhill_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.485Z'}]" +136,MVP,Baldhill_Dam.Flow.Inst.15Minutes.0.comp-gates,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.762Z'}]" +137,MVP,Baldhill_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:47.498Z'}]" +138,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:48.899142Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:48.793503Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:50.190682Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:50.030982Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:47.688705Z'}]" +139,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:01Z', 'last-update': '2025-06-16T18:39:50.206239Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:37Z', 'last-update': '2025-06-16T18:39:50.107133Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:18Z', 'last-update': '2025-06-16T18:39:51.534919Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:09Z', 'last-update': '2025-06-16T18:39:51.266095Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:37Z', 'last-update': '2025-06-16T18:39:49.126393Z'}]" +140,MVP,Baldhill_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:50.332Z'}]" +141,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:39:51.625464Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:39:51.519688Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:39:53.894761Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:39:54.05019Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:39:51.375083Z'}]" +142,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:24Z', 'last-update': '2025-06-16T18:39:52.836741Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:01Z', 'last-update': '2025-06-16T18:39:52.694102Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:42Z', 'last-update': '2025-06-16T18:39:55.374027Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:30Z', 'last-update': '2025-06-16T18:39:55.099601Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:04Z', 'last-update': '2025-06-16T18:39:52.557646Z'}]" +143,MVP,Baldhill_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:39:52.579Z'}]" +144,MVP,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.897Z'}]" +145,MVP,Baldhill_Dam.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.709Z'}]" +146,MVP,Baldhill_Dam.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.788Z'}]" +147,MVP,Baldhill_Dam.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:52.696Z'}]" +148,MVP,Baldhill_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-05-10T13:00:00Z', 'latest-time': '2025-05-25T13:00:00Z', 'last-update': '2025-06-16T18:39:52.83Z'}]" +149,MVP,Baldhill_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:53.893Z'}]" +150,MVP,Baldhill_Dam.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:54.224Z'}]" +151,MVP,Baldhill_Dam.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-26T03:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:54.089Z'}]" +152,MVP,Baldhill_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:39:53.946Z'}]" +153,MVP,Baldhill_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.182Z'}]" +154,MVP,Baldhill_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.503Z'}]" +155,MVP,Baldhill_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:55.518Z'}]" +156,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.503Z'}]" +157,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.764Z'}]" +158,MVP,Baldhill_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.012Z'}]" +159,MVP,Baldhill_Dam.Stor.Ave.15Minutes.2Hours.comp,m3,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:56.657Z'}]" +160,MVP,Baldhill_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:02.735Z'}]" +161,MVP,Baldhill_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:00.437Z'}]" +162,MVP,Baldhill_Dam.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:05.512Z'}]" +163,MVP,Baldhill_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:01.461Z'}]" +164,MVP,Baldhill_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:03.198Z'}]" +165,MVP,Baldhill_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:03.999Z'}]" +166,MVP,Baldhill_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T01:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.2Z'}]" +167,MVP,ChippewaDiv_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:09.118Z'}]" +168,MVP,ChippewaDiv_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T14:45:00Z', 'latest-time': '2025-05-21T17:15:00Z', 'last-update': '2025-06-16T18:40:07.942Z'}]" +169,MVP,ChippewaDiv_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:33.553Z'}]" +170,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.569Z'}]" +171,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.682Z'}]" +172,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.926Z'}]" +173,MVP,ChippewaDiv_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:35.743Z'}]" +174,MVP,ChippewaDiv_Dam-Tailwater.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-19T05:00:00Z', 'latest-time': '2025-06-12T05:00:00Z', 'last-update': '2025-06-16T18:40:35.989875Z'}]" +175,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:36.16Z'}]" +176,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:36.960811Z'}]" +177,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T20:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.852Z'}]" +178,MVP,ChippewaDiv_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T20:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.848Z'}]" +179,MVP,ChippewaDiv_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:18:25.512956Z'}]" +180,MVP,ChippewaDiv_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:25.809Z'}]" +181,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:38.696766Z'}]" +182,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:39.774Z'}]" +183,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:41.13Z'}]" +184,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:28.189Z'}]" +185,MVP,ChippewaDiv_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:30.627Z'}]" +186,MVP,ChippewaDiv_Dam-TainterGate.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:12.878Z'}]" +187,MVP,ChippewaDiv_Dam-TainterGate.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T14:45:00Z', 'latest-time': '2025-05-21T17:15:00Z', 'last-update': '2025-06-16T18:17:58.011Z'}]" +188,MVP,ChippewaDiv_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:43.374Z'}]" +189,MVP,ChippewaDiv_Dam.Depth-Frost-Thawed.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:30:00Z', 'latest-time': '2025-03-17T11:30:00Z', 'last-update': '2025-06-16T18:41:42.254Z'}]" +190,MVP,ChippewaDiv_Dam.Depth-Frost.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:30:00Z', 'latest-time': '2025-03-31T11:30:00Z', 'last-update': '2025-06-16T18:41:43.14Z'}]" +191,MVP,ChippewaDiv_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-04-30T12:00:00Z', 'last-update': '2025-06-16T18:41:44.356Z'}]" +192,MVP,ChippewaDiv_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-04-30T12:00:00Z', 'last-update': '2025-06-16T18:41:43.568Z'}]" +193,MVP,ChippewaDiv_Dam.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:44.762Z'}]" +194,MVP,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.543831Z'}]" +195,MVP,ChippewaDiv_Dam.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:47.172Z'}]" +196,MVP,ChippewaDiv_Dam.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:46.088Z'}]" +197,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:44.673Z'}]" +198,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-27T12:00:00Z', 'last-update': '2025-06-16T18:41:51.01957Z'}]" +199,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:41:52.443737Z'}]" +200,MVP,ChippewaDiv_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-10T12:00:00Z', 'last-update': '2025-06-16T18:41:52.05Z'}]" +201,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.275Z'}]" +202,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:52.189Z'}]" +203,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:25.420226Z'}]" +204,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.286Z'}]" +205,MVP,ChippewaDiv_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:53.48Z'}]" +206,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:57.09969Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:54.855506Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:55.956465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:54.679726Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:55.809561Z'}]" +207,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.733Z'}]" +208,MVP,ChippewaDiv_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:54.694Z'}]" +209,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:55.818022Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:55.838514Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:57.280079Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:57.098231Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:54.928116Z'}]" +210,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:55.819Z'}]" +211,MVP,ChippewaDiv_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:55.821Z'}]" +212,MVP,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:55.952Z'}]" +213,MVP,ChippewaDiv_Dam.Flow-Out.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:55.952Z'}]" +214,MVP,ChippewaDiv_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.006Z'}]" +215,MVP,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.093Z'}]" +216,MVP,ChippewaDiv_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.061Z'}]" +217,MVP,ChippewaDiv_Dam.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:56.121Z'}]" +218,MVP,ChippewaDiv_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.298Z'}]" +219,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:57.430999Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:58.313633Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:41:59.741333Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:59.580737Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:57.194951Z'}]" +220,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:57.444Z'}]" +221,MVP,ChippewaDiv_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:58.294Z'}]" +222,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:59.626113Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:41:58.704237Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:01.073864Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:00.844294Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:58.549139Z'}]" +223,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:58.698Z'}]" +224,MVP,ChippewaDiv_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:59.776Z'}]" +225,MVP,ChippewaDiv_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:59.642Z'}]" +226,MVP,ChippewaDiv_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:59.744Z'}]" +227,MVP,ChippewaDiv_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.859Z'}]" +228,MVP,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.868Z'}]" +229,MVP,ChippewaDiv_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:01.013Z'}]" +230,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.904Z'}]" +231,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.214Z'}]" +232,MVP,ChippewaDiv_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.219Z'}]" +233,MVP,ChippewaDiv_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:42:02.116Z'}]" +234,MVP,ChippewaDiv_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:00.904Z'}]" +235,MVP,ChippewaDiv_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:02.335Z'}]" +236,MVP,Clayton.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,America/Chicago,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:36:23.092Z'}]" +237,MVP,Clayton.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:24.294Z'}]" +238,MVP,Clayton.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:25.513Z'}]" +239,MVP,Clayton.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:42.861619Z'}]" +240,MVP,Clayton.Stage-Sensor02.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:26.779Z'}]" +241,MVP,Clayton.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:30.688Z'}]" +242,MVP,Clayton.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:31.791Z'}]" +243,MVP,Clayton.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.12Z'}]" +244,MVP,Clayton.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:31.91Z'}]" +245,MVP,Clayton.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:33.183Z'}]" +246,MVP,Clayton.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,America/Chicago,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-14T14:00:00Z', 'last-update': '2025-06-16T18:36:30.949Z'}]" +247,MVP,Cooperstown.Conc-OXYGEN.Inst.15Minutes.0.CEMVP-GOES-Raw,mg/l,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-21T20:00:00Z', 'last-update': '2025-06-16T18:41:25.519Z'}]" +248,MVP,Cooperstown.Cond.Inst.15Minutes.0.CEMVP-GOES-Raw,umho/cm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-10T15:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:21.647Z'}]" +249,MVP,Cooperstown.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:29.251Z'}]" +250,MVP,Cooperstown.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:22.959Z'}]" +251,MVP,Cooperstown.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:25.38Z'}]" +252,MVP,Cooperstown.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:05.214Z'}]" +253,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:30.692217Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:29.262443Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:31.665942Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:24.244412Z'}]" +254,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:05Z', 'last-update': '2025-06-16T18:41:34.238959Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:42Z', 'last-update': '2025-06-16T18:41:32.053423Z'}]" +255,MVP,Cooperstown.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:01:16.671Z'}]" +256,MVP,Cooperstown.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-12T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:19:04.987Z'}]" +257,MVP,Cooperstown.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:32.063Z'}]" +258,MVP,Cooperstown.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:19:11.246Z'}]" +259,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:34.375644Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:34.196326Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:34.27658Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:33.2504Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:33.043256Z'}]" +260,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:06Z', 'last-update': '2025-06-16T18:41:35.483537Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:58:42Z', 'last-update': '2025-06-16T18:41:35.791849Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:23Z', 'last-update': '2025-06-16T18:41:34.489929Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:13Z', 'last-update': '2025-06-16T18:41:36.740441Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:24:43Z', 'last-update': '2025-06-16T18:41:34.282181Z'}]" +261,MVP,Cooperstown.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:34.432Z'}]" +262,MVP,Cooperstown.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-19T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:34.57Z'}]" +263,MVP,Cooperstown.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-19T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:35.543Z'}]" +264,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:35.734828Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:35.675049Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:37.988756Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:35.470365Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:36.777554Z'}]" +265,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:27Z', 'last-update': '2025-06-16T18:41:38.2074Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:04Z', 'last-update': '2025-06-16T18:41:38.062446Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:45Z', 'last-update': '2025-06-16T18:41:39.432309Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:32Z', 'last-update': '2025-06-16T18:41:39.263304Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:07Z', 'last-update': '2025-06-16T18:41:37.005229Z'}]" +266,MVP,Cooperstown.Precip-RainAndMelt-Non_contrib.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:38.084Z'}]" +267,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:39.325588Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:39.28548Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:39.505689Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:40.537453Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:38.345677Z'}]" +268,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T15:04:26Z', 'last-update': '2025-06-16T18:41:40.591501Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:59:04Z', 'last-update': '2025-06-16T18:41:42.252535Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:01:44Z', 'last-update': '2025-06-16T18:41:42.097015Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T13:41:31Z', 'last-update': '2025-06-16T18:41:41.877405Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T15:25:07Z', 'last-update': '2025-06-16T18:41:39.559015Z'}]" +269,MVP,Cooperstown.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:40.525Z'}]" +270,MVP,Cooperstown.Stage.Ave.1Day.1Day.rev-USGS,m,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:41:40.538Z'}]" +271,MVP,Cooperstown.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.624Z'}]" +272,MVP,Cooperstown.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.778Z'}]" +273,MVP,Cooperstown.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.808Z'}]" +274,MVP,Cooperstown.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:41.809Z'}]" +275,MVP,Cooperstown.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-25T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.757Z'}]" +276,MVP,Cooperstown.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-10T15:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:40.884Z'}]" +277,MVP,Cooperstown.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:41.81Z'}]" +278,MVP,Cooperstown200.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:17:53.735Z'}]" +279,MVP,Cooperstown200.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:16.75Z'}]" +280,MVP,Cooperstown200.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:08.105Z'}]" +281,MVP,Cooperstown200.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-23T10:00:00Z', 'latest-time': '2025-05-29T09:45:00Z', 'last-update': '2025-06-16T18:40:08.211Z'}]" +282,MVP,DAWM5.Flow.Inst.15Minutes.0.Raw-MDNR,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:24.544Z'}]" +283,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:35:25.908151Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:35:26.041937Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:35:24.766193Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:35:24.66381Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:35:24.422245Z'}]" +284,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:27.166Z'}]" +285,MVP,DAWM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:27.042Z'}]" +286,MVP,DAWM5.Stage.Inst.15Minutes.0.Raw-MDNR,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:33.38Z'}]" +287,MVP,ELZM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-05-24T05:00:00Z', 'last-update': '2025-06-16T18:40:10.318Z'}]" +288,MVP,ELZM5.Flow.Inst.0.0.Raw-USGS,cms,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T16:54:00Z', 'latest-time': '2025-05-05T19:16:00Z', 'last-update': '2025-06-16T18:40:10.829Z'}]" +289,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:40:16.947828Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:40:18.045301Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:40:15.542405Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:40:14.342368Z'}]" +290,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:00.277Z'}]" +291,MVP,ELZM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:18:00.35Z'}]" +292,MVP,ELZM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:19.206Z'}]" +293,MVP,ELZM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:19.365Z'}]" +294,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:40:21.819255Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:40:19.501898Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:40:22.075077Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:40:20.503213Z'}]" +295,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.531Z'}]" +296,MVP,ELZM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:40:20.681Z'}]" +297,MVP,ELZM5.Stage.Inst.0.0.Raw-USGS,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-17T16:54:00Z', 'latest-time': '2025-05-05T19:16:00Z', 'last-update': '2025-06-16T18:40:20.621Z'}]" +298,MVP,ELZM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:21.768Z'}]" +299,MVP,ELZM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-25T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.96Z'}]" +300,MVP,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.599Z'}]" +301,MVP,Highway75_Dam-EmergencySpillway.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.837Z'}]" +302,MVP,Highway75_Dam-LeafGate.Elev.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.8Z'}]" +303,MVP,Highway75_Dam-LeafGate.Elev.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-19T16:15:00Z', 'latest-time': '2025-05-19T16:15:00Z', 'last-update': '2025-06-16T18:32:26.948Z'}]" +304,MVP,Highway75_Dam-LeafGate.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:27.175Z'}]" +305,MVP,Highway75_Dam-LeafGate.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:28.117Z'}]" +306,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:30.688Z'}]" +307,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.03Z'}]" +308,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.209Z'}]" +309,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.046Z'}]" +310,MVP,Highway75_Dam-LowFlow-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:38.529Z'}]" +311,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:33.202Z'}]" +312,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:33.364Z'}]" +313,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:03:00Z', 'latest-time': '2025-03-10T19:03:00Z', 'last-update': '2025-06-16T18:32:33.408Z'}]" +314,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:35.614Z'}]" +315,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:35.66Z'}]" +316,MVP,Highway75_Dam-LowFlow-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.548Z'}]" +317,MVP,Highway75_Dam-LowFlow.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:39.455Z'}]" +318,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.625Z'}]" +319,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:39.577Z'}]" +320,MVP,Highway75_Dam-LowFlow.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:40.687Z'}]" +321,MVP,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:44.357Z'}]" +322,MVP,Highway75_Dam-LowFlow.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.531Z'}]" +323,MVP,Highway75_Dam-LowFlow.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:44.866Z'}]" +324,MVP,Highway75_Dam-LowFlow.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-19T16:15:00Z', 'latest-time': '2025-05-19T16:15:00Z', 'last-update': '2025-06-16T18:32:47.096Z'}]" +325,MVP,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:47.255Z'}]" +326,MVP,Highway75_Dam-LowFlow.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.391Z'}]" +327,MVP,Highway75_Dam-LowFlow.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:49.634Z'}]" +328,MVP,Highway75_Dam-LowFlow.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:48.658Z'}]" +329,MVP,Highway75_Dam-LowFlow.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:49.723Z'}]" +330,MVP,Highway75_Dam-LowFlow.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:50.029Z'}]" +331,MVP,Highway75_Dam-LowFlow.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T18:58:00Z', 'latest-time': '2025-03-10T18:58:00Z', 'last-update': '2025-06-16T18:32:49.87Z'}]" +332,MVP,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:50.034Z'}]" +333,MVP,Highway75_Dam-LowFlow.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:51.08Z'}]" +334,MVP,Highway75_Dam-LowFlow.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:52.2Z'}]" +335,MVP,Highway75_Dam-LowFlow.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:53.64Z'}]" +336,MVP,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:57.329Z'}]" +337,MVP,Highway75_Dam-ServiceSpillway.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.85Z'}]" +338,MVP,Highway75_Dam-ServiceSpillway.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:32:56.248Z'}]" +339,MVP,Highway75_Dam-ServiceSpillway.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:55.987Z'}]" +340,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:27:00Z', 'latest-time': '2025-03-10T19:27:00Z', 'last-update': '2025-06-16T18:32:57.342Z'}]" +341,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.549Z'}]" +342,MVP,Highway75_Dam-ServiceSpillway.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.562Z'}]" +343,MVP,Highway75_Dam-ServiceSpillway.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:58.837Z'}]" +344,MVP,Highway75_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:01.266Z'}]" +345,MVP,Highway75_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:02.381Z'}]" +346,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:04.843056Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:03.883485Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:04.740973Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:04.972585Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:03.666933Z'}]" +347,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:04.881Z'}]" +348,MVP,Highway75_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:04.958Z'}]" +349,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:05.148Z'}]" +350,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:33:05.083Z'}]" +351,MVP,Highway75_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:33:06.095Z'}]" +352,MVP,Highway75_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:33:06.131Z'}]" +353,MVP,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.248Z'}]" +354,MVP,Highway75_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:07.474Z'}]" +355,MVP,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.157Z'}]" +356,MVP,Highway75_Dam.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:06.157Z'}]" +357,MVP,Highway75_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:07.251Z'}]" +358,MVP,Highway75_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:08.652Z'}]" +359,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:08.825761Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:08.814887Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:08.760374Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:11.055892Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:08.520874Z'}]" +360,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:08.862Z'}]" +361,MVP,Highway75_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.792Z'}]" +362,MVP,Highway75_Dam.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.939Z'}]" +363,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:13.746319Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:12.49672Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:12.646044Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:12.369118Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:10.026781Z'}]" +364,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:10.178Z'}]" +365,MVP,Highway75_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:09.997Z'}]" +366,MVP,Highway75_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:10.171Z'}]" +367,MVP,Highway75_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:10.209Z'}]" +368,MVP,Highway75_Dam.Flow-Out.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.141Z'}]" +369,MVP,Highway75_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.289Z'}]" +370,MVP,Highway75_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:11.315Z'}]" +371,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:12.337311Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:11.491397Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:13.956519Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:13.676051Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:11.279268Z'}]" +372,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.512Z'}]" +373,MVP,Highway75_Dam.Flow-Sim-RainOnPool.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.456Z'}]" +374,MVP,Highway75_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:12.656Z'}]" +375,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:13.905254Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:13.85237Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:13.819894Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:16.173579Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:13.647856Z'}]" +376,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:14.886Z'}]" +377,MVP,Highway75_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:15.027Z'}]" +378,MVP,Highway75_Dam.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:15.165Z'}]" +379,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:17.359793Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:16.494996Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:16.314894Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:15.187683Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:17.574924Z'}]" +380,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:16.447Z'}]" +381,MVP,Highway75_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.56Z'}]" +382,MVP,Highway75_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:17.369Z'}]" +383,MVP,Highway75_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.37Z'}]" +384,MVP,Highway75_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.984Z'}]" +385,MVP,Highway75_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.649Z'}]" +386,MVP,Highway75_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:17.588Z'}]" +387,MVP,Highway75_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:17.677Z'}]" +388,MVP,LFKM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-01T05:00:00Z', 'latest-time': '2025-06-05T05:00:00Z', 'last-update': '2025-06-16T18:42:59.425Z'}]" +389,MVP,LFKM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.717Z'}]" +390,MVP,LFKM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.592Z'}]" +391,MVP,LFKM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-20T17:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.785Z'}]" +392,MVP,LFKM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-04-20T17:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:00.749Z'}]" +393,MVP,LFKM5.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:03.512Z'}]" +394,MVP,LFKM5.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:04.706Z'}]" +395,MVP,LFKM5.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:06.911Z'}]" +396,MVP,LFKM5.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:05.633Z'}]" +397,MVP,LFKM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:08.212Z'}]" +398,MVP,LFKM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:11.227Z'}]" +399,MVP,LFKM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:12.217Z'}]" +400,MVP,LFKM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:13.62Z'}]" +401,MVP,LFKM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-06-06T05:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:12.502Z'}]" +402,MVP,LFKM5.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:14.644Z'}]" +403,MVP,LFKM5.Volt.Inst.15Minutes.0.CEMVP-GOES-Raw,volt,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:30:00Z', 'latest-time': '2025-06-12T23:30:00Z', 'last-update': '2025-06-16T18:43:15.972557Z'}]" +404,MVP,LockDam_02-Powerhouse.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:49.179Z'}]" +405,MVP,LockDam_02-Powerhouse.Power.Inst.15Minutes.0.CEMVP-ProjectEntry,MW,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:52.765Z'}]" +406,MVP,LockDam_02-Powerhouse.Power.Inst.~15Minutes.0.CEMVP-ProjectEntry,MW,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:06.867Z'}]" +407,MVP,LockDam_02-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:22.487Z'}]" +408,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:22.536Z'}]" +409,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:23.684Z'}]" +410,MVP,LockDam_02-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:25.03Z'}]" +411,MVP,LockDam_02-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:24.844Z'}]" +412,MVP,LockDam_02-Tailwater.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:23.819Z'}]" +413,MVP,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:25.138Z'}]" +414,MVP,LockDam_02-Tailwater.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.254Z'}]" +415,MVP,LockDam_02-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:26.09Z'}]" +416,MVP,LockDam_02-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:26.147Z'}]" +417,MVP,LockDam_02-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T11:30:00Z', 'latest-time': '2025-05-28T10:15:00Z', 'last-update': '2025-06-16T18:43:26.25Z'}]" +418,MVP,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:26.444Z'}]" +419,MVP,LockDam_02-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:32.846Z'}]" +420,MVP,LockDam_02-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.242Z'}]" +421,MVP,LockDam_02-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:28.383Z'}]" +422,MVP,LockDam_02-TainterGate01.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:33.268Z'}]" +423,MVP,LockDam_02-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:34.508Z'}]" +424,MVP,LockDam_02-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:30.857Z'}]" +425,MVP,LockDam_02-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:27.651Z'}]" +426,MVP,LockDam_02-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:32.858Z'}]" +427,MVP,LockDam_02-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:43:32.439Z'}]" +428,MVP,LockDam_02-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:53.105Z'}]" +429,MVP,LockDam_02-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:53.302Z'}]" +430,MVP,LockDam_02-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:38:54.276Z'}]" +431,MVP,LockDam_02-TainterGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:02:59.43Z'}]" +432,MVP,LockDam_02-TainterGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:03:00.732Z'}]" +433,MVP,LockDam_02-TainterGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:02:59.716Z'}]" +434,MVP,LockDam_02-TainterGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:55.625Z'}]" +435,MVP,LockDam_02-TainterGate05.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:54.6Z'}]" +436,MVP,LockDam_02-TainterGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:57.009Z'}]" +437,MVP,LockDam_02-TainterGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:38:55.695Z'}]" +438,MVP,LockDam_02-TainterGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:36.656Z'}]" +439,MVP,LockDam_02-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:38.858Z'}]" +440,MVP,LockDam_02-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:43:38.77Z'}]" +441,MVP,LockDam_02-TainterGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:04.413Z'}]" +442,MVP,LockDam_02-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:06.621Z'}]" +443,MVP,LockDam_02-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:04.465Z'}]" +444,MVP,LockDam_02-TainterGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.694Z'}]" +445,MVP,LockDam_02-TainterGate08.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.684Z'}]" +446,MVP,LockDam_02-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.807Z'}]" +447,MVP,LockDam_02-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:44:01.581Z'}]" +448,MVP,LockDam_02-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:06.565Z'}]" +449,MVP,LockDam_02-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:17:47.959Z'}]" +450,MVP,LockDam_02-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:17:47.567Z'}]" +451,MVP,LockDam_02-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:04.559Z'}]" +452,MVP,LockDam_02-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:01.954Z'}]" +453,MVP,LockDam_02-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:44:01.887Z'}]" +454,MVP,LockDam_02-TainterGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:09.201Z'}]" +455,MVP,LockDam_02-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:10.422Z'}]" +456,MVP,LockDam_02-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:40:09.314Z'}]" +457,MVP,LockDam_02-TainterGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:46.793Z'}]" +458,MVP,LockDam_02-TainterGate12.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:47.081Z'}]" +459,MVP,LockDam_02-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:46.886Z'}]" +460,MVP,LockDam_02-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:47.878Z'}]" +461,MVP,LockDam_02-TainterGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:34.978Z'}]" +462,MVP,LockDam_02-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:45.13Z'}]" +463,MVP,LockDam_02-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T17:57:38.273Z'}]" +464,MVP,LockDam_02-TainterGate14.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:49.102Z'}]" +465,MVP,LockDam_02-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:49.259Z'}]" +466,MVP,LockDam_02-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:48.107Z'}]" +467,MVP,LockDam_02-TainterGate15.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:48.724Z'}]" +468,MVP,LockDam_02-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:13:48.722Z'}]" +469,MVP,LockDam_02-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:13:47.717Z'}]" +470,MVP,LockDam_02-TainterGate16.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.612Z'}]" +471,MVP,LockDam_02-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.413Z'}]" +472,MVP,LockDam_02-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:58.491Z'}]" +473,MVP,LockDam_02-TainterGate17.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:11.496Z'}]" +474,MVP,LockDam_02-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:15.665Z'}]" +475,MVP,LockDam_02-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:36:12.925Z'}]" +476,MVP,LockDam_02-TainterGate18.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.515Z'}]" +477,MVP,LockDam_02-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:59.654Z'}]" +478,MVP,LockDam_02-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:42:59.413Z'}]" +479,MVP,LockDam_02-TainterGate19.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:13.158Z'}]" +480,MVP,LockDam_02-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:15.42Z'}]" +481,MVP,LockDam_02-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:14:15.25Z'}]" +482,MVP,LockDam_02-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:58.092Z'}]" +483,MVP,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:33.552Z'}]" +484,MVP,LockDam_02-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:59.428Z'}]" +485,MVP,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:59.617Z'}]" +486,MVP,LockDam_02-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:00.552Z'}]" +487,MVP,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:57.03Z'}]" +488,MVP,LockDam_02-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:39:04.742Z'}]" +489,MVP,LockDam_02-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:59.419Z'}]" +490,MVP,LockDam_02-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:58.212Z'}]" +491,MVP,LockDam_02-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T18:00:00Z', 'latest-time': '2025-06-11T12:30:00Z', 'last-update': '2025-06-16T18:34:57.787Z'}]" +492,MVP,LockDam_02.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T10:20:00Z', 'last-update': '2025-06-16T18:44:06.894Z'}]" +493,MVP,LockDam_02.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:02.883Z'}]" +494,MVP,LockDam_02.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:03.187Z'}]" +495,MVP,LockDam_02.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-04T11:30:00Z', 'latest-time': '2025-06-08T10:20:00Z', 'last-update': '2025-06-16T18:44:03.147Z'}]" +496,MVP,LockDam_02.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:44:03.192Z'}]" +497,MVP,LockDam_02.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:44:04.137Z'}]" +498,MVP,LockDam_02.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:05.389Z'}]" +499,MVP,LockDam_02.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:04.427Z'}]" +500,MVP,LockDam_02.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:07:51.057887Z'}]" +501,MVP,LockDam_02.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:07.898Z'}]" +502,MVP,LockDam_02.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:36:07.432Z'}]" +503,MVP,LockDam_02.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:36:07.243Z'}]" +504,MVP,LockDam_02.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:13.383Z'}]" +505,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:14.409262Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:14.680085Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:15.755201Z'}]" +506,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:07:45.865625Z'}]" +507,MVP,LockDam_02.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T17:36:17.219Z'}]" +508,MVP,LockDam_02.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:36:17.430584Z'}]" +509,MVP,LockDam_02.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:15.805Z'}]" +510,MVP,LockDam_02.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:16.042Z'}]" +511,MVP,LockDam_02.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:15.922Z'}]" +512,MVP,LockDam_02.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.875208Z'}]" +513,MVP,LockDam_02.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:16.81Z'}]" +514,MVP,LockDam_02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:28.242111Z'}]" +515,MVP,LockDam_02.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:28.302635Z'}]" +516,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:20.611972Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:44:20.617986Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:19.487606Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:19.56505Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:18.020899Z'}]" +517,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.86421Z'}]" +518,MVP,LockDam_02.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:44:20.741Z'}]" +519,MVP,LockDam_02.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.903Z'}]" +520,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:22.151515Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:44:22.219243Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:44:24.77091Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:24.428343Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:21.932781Z'}]" +521,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:23.152Z'}]" +522,MVP,LockDam_02.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:44:23.388Z'}]" +523,MVP,LockDam_02.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:23.568Z'}]" +524,MVP,LockDam_02.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.489Z'}]" +525,MVP,LockDam_02.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:23.588Z'}]" +526,MVP,LockDam_02.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:24.618Z'}]" +527,MVP,LockDam_02.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-06T11:30:00Z', 'latest-time': '2025-05-28T10:15:00Z', 'last-update': '2025-06-16T18:44:24.761Z'}]" +528,MVP,LockDam_02.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:25.845Z'}]" +529,MVP,LockDam_02.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:27.237Z'}]" +530,MVP,LockDam_02.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:26.014Z'}]" +531,MVP,LockDam_02.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:27.021Z'}]" +532,MVP,LockDam_02.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:25.921Z'}]" +533,MVP,LockDam_02.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.233Z'}]" +534,MVP,LockDam_02.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.51Z'}]" +535,MVP,LockDam_02.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:44:28.737Z'}]" +536,MVP,LockDam_02.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:29.747Z'}]" +537,MVP,LockDam_02.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:30.939Z'}]" +538,MVP,LockDam_02.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T10:00:00Z', 'last-update': '2025-06-16T18:44:29.847Z'}]" +539,MVP,LockDam_02.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:32.231657Z'}]" +540,MVP,LockDam_05-RollerGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:48.711Z'}]" +541,MVP,LockDam_05-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:49.823Z'}]" +542,MVP,LockDam_05-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:47.427Z'}]" +543,MVP,LockDam_05-RollerGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:51.017Z'}]" +544,MVP,LockDam_05-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:52.213Z'}]" +545,MVP,LockDam_05-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:50.025Z'}]" +546,MVP,LockDam_05-RollerGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:56.021Z'}]" +547,MVP,LockDam_05-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:40.611Z'}]" +548,MVP,LockDam_05-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:56.225Z'}]" +549,MVP,LockDam_05-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:49.553708Z'}]" +550,MVP,LockDam_05-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:57.238Z'}]" +551,MVP,LockDam_05-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:58.728Z'}]" +552,MVP,LockDam_05-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:58.909Z'}]" +553,MVP,LockDam_05-RollerGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:07.67342Z'}]" +554,MVP,LockDam_05-RollerGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:59.832Z'}]" +555,MVP,LockDam_05-RollerGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:59.989Z'}]" +556,MVP,LockDam_05-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:10.200035Z'}]" +557,MVP,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:07.496429Z'}]" +558,MVP,LockDam_05-RollerGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:58.77346Z'}]" +559,MVP,LockDam_05-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:01.427Z'}]" +560,MVP,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:02.333Z'}]" +561,MVP,LockDam_05-RollerGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:03.95Z'}]" +562,MVP,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:03.568Z'}]" +563,MVP,LockDam_05-RollerGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:02.795Z'}]" +564,MVP,LockDam_05-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:08.734Z'}]" +565,MVP,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.79771Z'}]" +566,MVP,LockDam_05-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:09.954Z'}]" +567,MVP,LockDam_05-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:11.535Z'}]" +568,MVP,LockDam_05-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:11.494Z'}]" +569,MVP,LockDam_05-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:12.446109Z'}]" +570,MVP,LockDam_05-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T15:00:00Z', 'latest-time': '2025-06-01T16:45:00Z', 'last-update': '2025-06-16T17:58:33.035452Z'}]" +571,MVP,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:54.92Z'}]" +572,MVP,LockDam_05-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:13.745Z'}]" +573,MVP,LockDam_05-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:14.043Z'}]" +574,MVP,LockDam_05-TainterGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:19.457Z'}]" +575,MVP,LockDam_05-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:21.672Z'}]" +576,MVP,LockDam_05-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:20.762Z'}]" +577,MVP,LockDam_05-TainterGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:16.468Z'}]" +578,MVP,LockDam_05-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:16.659Z'}]" +579,MVP,LockDam_05-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:16.34Z'}]" +580,MVP,LockDam_05-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:47.288046Z'}]" +581,MVP,LockDam_05-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:58:19.848Z'}]" +582,MVP,LockDam_05-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T17:58:18.97Z'}]" +583,MVP,LockDam_05-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:08.660858Z'}]" +584,MVP,LockDam_05-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.85Z'}]" +585,MVP,LockDam_05-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:16.653Z'}]" +586,MVP,LockDam_05-TainterGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:42.298Z'}]" +587,MVP,LockDam_05-TainterGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:44.584Z'}]" +588,MVP,LockDam_05-TainterGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:43.502Z'}]" +589,MVP,LockDam_05-TainterGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:18.054Z'}]" +590,MVP,LockDam_05-TainterGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:17.963Z'}]" +591,MVP,LockDam_05-TainterGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:20.467Z'}]" +592,MVP,LockDam_05-TainterGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:57.924Z'}]" +593,MVP,LockDam_05-TainterGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:00.327Z'}]" +594,MVP,LockDam_05-TainterGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:02.92Z'}]" +595,MVP,LockDam_05-TainterGate14.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:25.251Z'}]" +596,MVP,LockDam_05-TainterGate14.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:19.052Z'}]" +597,MVP,LockDam_05-TainterGate14.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:22.877Z'}]" +598,MVP,LockDam_05-TainterGate15.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:17.14Z'}]" +599,MVP,LockDam_05-TainterGate15.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:18.17Z'}]" +600,MVP,LockDam_05-TainterGate15.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:18.089Z'}]" +601,MVP,LockDam_05-TainterGate16.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:35.525459Z'}]" +602,MVP,LockDam_05-TainterGate16.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:25.459Z'}]" +603,MVP,LockDam_05-TainterGate16.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:23.88Z'}]" +604,MVP,LockDam_05-TainterGate17.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:19.756Z'}]" +605,MVP,LockDam_05-TainterGate17.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:20.751Z'}]" +606,MVP,LockDam_05-TainterGate17.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:22.136Z'}]" +607,MVP,LockDam_05-TainterGate18.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:24.075Z'}]" +608,MVP,LockDam_05-TainterGate18.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:08.951Z'}]" +609,MVP,LockDam_05-TainterGate18.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:27.724Z'}]" +610,MVP,LockDam_05-TainterGate19.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:22.273Z'}]" +611,MVP,LockDam_05-TainterGate19.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:23.514Z'}]" +612,MVP,LockDam_05-TainterGate19.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:23.202Z'}]" +613,MVP,LockDam_05-TainterGate20.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:27.771102Z'}]" +614,MVP,LockDam_05-TainterGate20.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:29.379Z'}]" +615,MVP,LockDam_05-TainterGate20.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:29.354Z'}]" +616,MVP,LockDam_05-TainterGate21.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:33.556Z'}]" +617,MVP,LockDam_05-TainterGate21.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:34.776Z'}]" +618,MVP,LockDam_05-TainterGate21.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:34.639Z'}]" +619,MVP,LockDam_05-TainterGate22.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:59.413Z'}]" +620,MVP,LockDam_05-TainterGate22.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:30.294Z'}]" +621,MVP,LockDam_05-TainterGate23.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:23.679Z'}]" +622,MVP,LockDam_05-TainterGate23.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:23.798Z'}]" +623,MVP,LockDam_05-TainterGate23.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:32:24.178Z'}]" +624,MVP,LockDam_05-TainterGate24.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.5Z'}]" +625,MVP,LockDam_05-TainterGate24.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.795Z'}]" +626,MVP,LockDam_05-TainterGate24.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:30.704Z'}]" +627,MVP,LockDam_05-TainterGate25.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:19.925Z'}]" +628,MVP,LockDam_05-TainterGate25.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.372Z'}]" +629,MVP,LockDam_05-TainterGate25.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:33:19.92Z'}]" +630,MVP,LockDam_05-TainterGate26.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.621Z'}]" +631,MVP,LockDam_05-TainterGate26.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:32.772Z'}]" +632,MVP,LockDam_05-TainterGate26.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:31.592Z'}]" +633,MVP,LockDam_05-TainterGate27.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:34.066Z'}]" +634,MVP,LockDam_05-TainterGate27.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:11:50.844Z'}]" +635,MVP,LockDam_05-TainterGate27.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:11:50.661Z'}]" +636,MVP,LockDam_05-TainterGate28.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:44.217699Z'}]" +637,MVP,LockDam_05-TainterGate28.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:31.969Z'}]" +638,MVP,LockDam_05-TainterGate28.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:38.261Z'}]" +639,MVP,LockDam_05-TainterGate29.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.954Z'}]" +640,MVP,LockDam_05-TainterGate29.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.982Z'}]" +641,MVP,LockDam_05-TainterGate29.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:16.886Z'}]" +642,MVP,LockDam_05-TainterGate30.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:58:08.617Z'}]" +643,MVP,LockDam_05-TainterGate30.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:22.111Z'}]" +644,MVP,LockDam_05-TainterGate31.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:48.66511Z'}]" +645,MVP,LockDam_05-TainterGate31.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:40.998Z'}]" +646,MVP,LockDam_05-TainterGate31.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:37.182Z'}]" +647,MVP,LockDam_05-TainterGate32.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:52.888841Z'}]" +648,MVP,LockDam_05-TainterGate32.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:40.16Z'}]" +649,MVP,LockDam_05-TainterGate32.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:35:38.565Z'}]" +650,MVP,LockDam_05-TainterGate33.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:57.754431Z'}]" +651,MVP,LockDam_05-TainterGate33.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:31:03.722585Z'}]" +652,MVP,LockDam_05-TainterGate33.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:13:47.713Z'}]" +653,MVP,LockDam_05-TainterGate34.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:16.532Z'}]" +654,MVP,LockDam_05-TainterGate34.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:17.963Z'}]" +655,MVP,LockDam_05-TainterGate34.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:36:16.558Z'}]" +656,MVP,LockDam_05-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:00.859541Z'}]" +657,MVP,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:03.572481Z'}]" +658,MVP,LockDam_05-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:17.517279Z'}]" +659,MVP,LockDam_05-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:18.724126Z'}]" +660,MVP,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:51.939Z'}]" +661,MVP,LockDam_05-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:08.281Z'}]" +662,MVP,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.125Z'}]" +663,MVP,LockDam_05-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:51.824Z'}]" +664,MVP,LockDam_05-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:37.871Z'}]" +665,MVP,LockDam_05-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:39.164Z'}]" +666,MVP,LockDam_05-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-11T14:00:00Z', 'last-update': '2025-06-16T18:37:37.988Z'}]" +667,MVP,LockDam_05.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T13:00:00Z', 'latest-time': '2025-06-04T11:00:00Z', 'last-update': '2025-06-16T18:37:39.491Z'}]" +668,MVP,LockDam_05.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:20.233Z'}]" +669,MVP,LockDam_05.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:40.785Z'}]" +670,MVP,LockDam_05.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T13:00:00Z', 'latest-time': '2025-06-03T11:00:00Z', 'last-update': '2025-06-16T18:15:20.453Z'}]" +671,MVP,LockDam_05.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:15:25.296Z'}]" +672,MVP,LockDam_05.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-04T12:00:00Z', 'last-update': '2025-06-16T18:37:41.842Z'}]" +673,MVP,LockDam_05.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:15:26.758Z'}]" +674,MVP,LockDam_05.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:43.309Z'}]" +675,MVP,LockDam_05.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:58:45.348Z'}]" +676,MVP,LockDam_05.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.47Z'}]" +677,MVP,LockDam_05.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.708Z'}]" +678,MVP,LockDam_05.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:45.81Z'}]" +679,MVP,LockDam_05.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:31.940469Z'}]" +680,MVP,LockDam_05.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:15:30.586Z'}]" +681,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:48.314133Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:49.443519Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:45.928667Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:48.087896Z'}]" +682,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:48.367Z'}]" +683,MVP,LockDam_05.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:46.91Z'}]" +684,MVP,LockDam_05.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:51.001Z'}]" +685,MVP,LockDam_05.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:47.115Z'}]" +686,MVP,LockDam_05.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.317Z'}]" +687,MVP,LockDam_05.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.417Z'}]" +688,MVP,LockDam_05.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:50.631Z'}]" +689,MVP,LockDam_05.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:48.403Z'}]" +690,MVP,LockDam_05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:49.626Z'}]" +691,MVP,LockDam_05.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:53.321Z'}]" +692,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:54.4365Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:52.132615Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:53.165455Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:53.154478Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:37:50.651719Z'}]" +693,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:55.681Z'}]" +694,MVP,LockDam_05.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:54.813Z'}]" +695,MVP,LockDam_05.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:55.925Z'}]" +696,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:37:56.106918Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:37:56.070753Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:37:56.999618Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:37:55.88586Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:37:56.106895Z'}]" +697,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:57.009Z'}]" +698,MVP,LockDam_05.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:37:56.994Z'}]" +699,MVP,LockDam_05.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:57.02Z'}]" +700,MVP,LockDam_05.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.243Z'}]" +701,MVP,LockDam_05.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:57.257Z'}]" +702,MVP,LockDam_05.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.165Z'}]" +703,MVP,LockDam_05.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T15:00:00Z', 'latest-time': '2025-06-01T16:45:00Z', 'last-update': '2025-06-16T18:37:58.321Z'}]" +704,MVP,LockDam_05.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:58.535Z'}]" +705,MVP,LockDam_05.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.636Z'}]" +706,MVP,LockDam_05.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.734Z'}]" +707,MVP,LockDam_05.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:37:59.839Z'}]" +708,MVP,LockDam_05.Temp-Water.Ave.1Day.1Day.merged,C,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:37:59.479Z'}]" +709,MVP,LockDam_05.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:02.416Z'}]" +710,MVP,LockDam_05.Temp-Water.Inst.15Minutes.0.merged,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:02.007Z'}]" +711,MVP,LockDam_05.Temp-Water.Inst.~1Day.0.Raw-NWS-IEM,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T06:00:00Z', 'latest-time': '2025-06-12T05:00:00Z', 'last-update': '2025-06-16T18:38:02.018Z'}]" +712,MVP,LockDam_05.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:03.277Z'}]" +713,MVP,LockDam_05a-CrookedSlough.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:02.006Z'}]" +714,MVP,LockDam_05a-CrookedSlough.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:05.924Z'}]" +715,MVP,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:05.857Z'}]" +716,MVP,LockDam_05a-CrookedSlough.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:06.244Z'}]" +717,MVP,LockDam_05a-CrookedSlough.Temp-Water.Inst.1Hour.0.CEMVP-GOES-Raw,C,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:08.535Z'}]" +718,MVP,LockDam_05a-CrookedSlough.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:09.746Z'}]" +719,MVP,LockDam_05a-RollerGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.458Z'}]" +720,MVP,LockDam_05a-RollerGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:54.288Z'}]" +721,MVP,LockDam_05a-RollerGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:33:53.228Z'}]" +722,MVP,LockDam_05a-RollerGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.05Z'}]" +723,MVP,LockDam_05a-RollerGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.047Z'}]" +724,MVP,LockDam_05a-RollerGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:09.936Z'}]" +725,MVP,LockDam_05a-RollerGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:11.263Z'}]" +726,MVP,LockDam_05a-RollerGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.153Z'}]" +727,MVP,LockDam_05a-RollerGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:11.027Z'}]" +728,MVP,LockDam_05a-RollerGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:14:54.621557Z'}]" +729,MVP,LockDam_05a-RollerGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:47.397Z'}]" +730,MVP,LockDam_05a-RollerGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:36:47.049Z'}]" +731,MVP,LockDam_05a-RollerGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:18.779675Z'}]" +732,MVP,LockDam_05a-RollerGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:09.818Z'}]" +733,MVP,LockDam_05a-RollerGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:11.286Z'}]" +734,MVP,LockDam_05a-RollerGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.59Z'}]" +735,MVP,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:12.594Z'}]" +736,MVP,LockDam_05a-RollerGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.771Z'}]" +737,MVP,LockDam_05a-RollerGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.895Z'}]" +738,MVP,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:14.707Z'}]" +739,MVP,LockDam_05a-RollerGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:13.943Z'}]" +740,MVP,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:23.595Z'}]" +741,MVP,LockDam_05a-RollerGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:13.12Z'}]" +742,MVP,LockDam_05a-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:33:18.926Z'}]" +743,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.19Z'}]" +744,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:20.311Z'}]" +745,MVP,LockDam_05a-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:22.717Z'}]" +746,MVP,LockDam_05a-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:35.242Z'}]" +747,MVP,LockDam_05a-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:22.66Z'}]" +748,MVP,LockDam_05a-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-10T12:00:00Z', 'last-update': '2025-06-16T18:33:21.646Z'}]" +749,MVP,LockDam_05a-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:25.501Z'}]" +750,MVP,LockDam_05a-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-05-16T18:14:00Z', 'latest-time': '2025-06-11T22:15:00Z', 'last-update': '2025-06-16T18:33:26.492Z'}]" +751,MVP,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:26.583Z'}]" +752,MVP,LockDam_05a-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:31.394Z'}]" +753,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:27.577Z'}]" +754,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:31.606Z'}]" +755,MVP,LockDam_05a-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:37.71Z'}]" +756,MVP,LockDam_05a-TainterGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:22.626006Z'}]" +757,MVP,LockDam_05a-TainterGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:32:38.83779Z'}]" +758,MVP,LockDam_05a-TainterGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:59:13.209Z'}]" +759,MVP,LockDam_05a-TainterGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:14.609Z'}]" +760,MVP,LockDam_05a-TainterGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:59:14.374Z'}]" +761,MVP,LockDam_05a-TainterGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:32:33.58Z'}]" +762,MVP,LockDam_05a-TainterGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T17:32:31.046Z'}]" +763,MVP,LockDam_05a-TainterGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:34.15643Z'}]" +764,MVP,LockDam_05a-TainterGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:25.173Z'}]" +765,MVP,LockDam_05a-TainterGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:38:24.949Z'}]" +766,MVP,LockDam_05a-TainterGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:32.753012Z'}]" +767,MVP,LockDam_05a-TainterGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:15.671Z'}]" +768,MVP,LockDam_05a-TainterGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:16:13.528Z'}]" +769,MVP,LockDam_05a-TainterGates.Flow-PerFootOpen.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.333Z'}]" +770,MVP,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.6Z'}]" +771,MVP,LockDam_05a-TainterGates.Flow.Inst.15Minutes.0.test,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.673Z'}]" +772,MVP,LockDam_05a-TainterGates.Opening-MaxAllow.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:26.558Z'}]" +773,MVP,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.616Z'}]" +774,MVP,LockDam_05a-TainterGates.Opening-Normal.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.485Z'}]" +775,MVP,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:27.77Z'}]" +776,MVP,LockDam_05a-TainterGates.Opening-Submerged.Inst.15Minutes.0.test,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:35.381Z'}]" +777,MVP,LockDam_05a-TainterValves.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:54.826804Z'}]" +778,MVP,LockDam_05a-TainterValves.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:36:45.902Z'}]" +779,MVP,LockDam_05a-TainterValves.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T14:00:00Z', 'latest-time': '2025-06-10T13:00:00Z', 'last-update': '2025-06-16T18:36:44.619Z'}]" +780,MVP,LockDam_05a.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T11:00:00Z', 'last-update': '2025-06-16T18:38:34.218Z'}]" +781,MVP,LockDam_05a.Code-OpenRiver.Inst.15Minutes.0.comp,n/a,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:59:29.956107Z'}]" +782,MVP,LockDam_05a.Code-OpenRiver.Inst.1Hour.0.Fcst-CEMVP,n/a,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:36.278Z'}]" +783,MVP,LockDam_05a.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T11:00:00Z', 'last-update': '2025-06-16T18:38:35.195Z'}]" +784,MVP,LockDam_05a.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:42.765512Z'}]" +785,MVP,LockDam_05a.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:41.443Z'}]" +786,MVP,LockDam_05a.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:59:24.776Z'}]" +787,MVP,LockDam_05a.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T17:32:42.435Z'}]" +788,MVP,LockDam_05a.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:37.624699Z'}]" +789,MVP,LockDam_05a.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:39.210636Z'}]" +790,MVP,LockDam_05a.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:39.04818Z'}]" +791,MVP,LockDam_05a.Elev.Inst.1Hour.0.Regulating,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:35.129195Z'}]" +792,MVP,LockDam_05a.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:16:31.194Z'}]" +793,MVP,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:39.173Z'}]" +794,MVP,LockDam_05a.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:39.272Z'}]" +795,MVP,LockDam_05a.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-08T12:00:00Z', 'last-update': '2025-06-16T18:38:39.222Z'}]" +796,MVP,LockDam_05a.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.171Z'}]" +797,MVP,LockDam_05a.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.446Z'}]" +798,MVP,LockDam_05a.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.119Z'}]" +799,MVP,LockDam_05a.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.394Z'}]" +800,MVP,LockDam_05a.Flow.Ave.1Day.1Day.merged,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:40.118Z'}]" +801,MVP,LockDam_05a.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:40.496Z'}]" +802,MVP,LockDam_05a.Flow.Inst.15Minutes.0.merged,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:41.4Z'}]" +803,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:38:41.605968Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:38:41.691065Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:38:41.527687Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:38:42.755259Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:38:41.379166Z'}]" +804,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:42.784Z'}]" +805,MVP,LockDam_05a.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:42.782Z'}]" +806,MVP,LockDam_05a.Head.Inst.15Minutes.0.comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:44.092Z'}]" +807,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:38:45.25246Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:38:44.3434Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:38:44.285624Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:38:46.678264Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:38:44.02367Z'}]" +808,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:45.492Z'}]" +809,MVP,LockDam_05a.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:38:45.387Z'}]" +810,MVP,LockDam_05a.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:45.387Z'}]" +811,MVP,LockDam_05a.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.737Z'}]" +812,MVP,LockDam_05a.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:46.678Z'}]" +813,MVP,LockDam_05a.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:46.616Z'}]" +814,MVP,LockDam_05a.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-05-16T18:06:00Z', 'latest-time': '2025-05-16T18:06:00Z', 'last-update': '2025-06-16T18:38:47.965Z'}]" +815,MVP,LockDam_05a.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.799Z'}]" +816,MVP,LockDam_05a.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:47.899Z'}]" +817,MVP,LockDam_05a.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:49.134Z'}]" +818,MVP,LockDam_05a.Temp-Air.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:48Z'}]" +819,MVP,LockDam_05a.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:47.722Z'}]" +820,MVP,LockDam_05a.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:48.202Z'}]" +821,MVP,LockDam_05a.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:38:51.986Z'}]" +822,MVP,LockDam_05a.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:38:49.399Z'}]" +823,MVP,MissHW_Gull-Fishway.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:35.052Z'}]" +824,MVP,MissHW_Gull-Fishway.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:43:33.948Z'}]" +825,MVP,MissHW_Gull-Lake.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:03.159Z'}]" +826,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:00.621Z'}]" +827,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:04.235Z'}]" +828,MVP,MissHW_Gull-Lake.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:04.431Z'}]" +829,MVP,MissHW_Gull-Lake.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:05.404Z'}]" +830,MVP,MissHW_Gull-Lake.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:07.137Z'}]" +831,MVP,MissHW_Gull-Lake.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T19:43:00Z', 'latest-time': '2025-04-21T17:06:00Z', 'last-update': '2025-06-16T18:35:05.827Z'}]" +832,MVP,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:07.127Z'}]" +833,MVP,MissHW_Gull-Lake.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:09.34Z'}]" +834,MVP,MissHW_Gull-Lake.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:06.723Z'}]" +835,MVP,MissHW_Gull-Lake.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:10.521Z'}]" +836,MVP,MissHW_Gull-Lake.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:11.921Z'}]" +837,MVP,MissHW_Gull-SlideGate01.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.641Z'}]" +838,MVP,MissHW_Gull-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:18.979Z'}]" +839,MVP,MissHW_Gull-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:33:18.718Z'}]" +840,MVP,MissHW_Gull-SlideGate02.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:27.844Z'}]" +841,MVP,MissHW_Gull-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:28.864Z'}]" +842,MVP,MissHW_Gull-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:27.859Z'}]" +843,MVP,MissHW_Gull-SlideGate03.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:18.451Z'}]" +844,MVP,MissHW_Gull-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:20.95Z'}]" +845,MVP,MissHW_Gull-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:35:18.125Z'}]" +846,MVP,MissHW_Gull-SlideGate04.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:23.85Z'}]" +847,MVP,MissHW_Gull-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:26.648Z'}]" +848,MVP,MissHW_Gull-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:30.232Z'}]" +849,MVP,MissHW_Gull-SlideGate05.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:28.723Z'}]" +850,MVP,MissHW_Gull-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:29.646Z'}]" +851,MVP,MissHW_Gull-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:35:37.143Z'}]" +852,MVP,MissHW_Gull-StopLog01.Flow.Inst.1Hour.0.comp,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:29.044Z'}]" +853,MVP,MissHW_Gull-StopLog01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:29.178Z'}]" +854,MVP,MissHW_Gull-StopLog01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:42:28.954Z'}]" +855,MVP,MissHW_Gull-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:13:23.351Z'}]" +856,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.merged-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:11.943Z'}]" +857,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NAVD88,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:12.132Z'}]" +858,MVP,MissHW_Gull-Tailwater.Elev.Inst.1Hour.0.rev-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:13.22Z'}]" +859,MVP,MissHW_Gull-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:13.227Z'}]" +860,MVP,MissHW_Gull-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:35:15.73Z'}]" +861,MVP,MissHW_Gull-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:14.441Z'}]" +862,MVP,MissHW_Gull-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T18:18:00Z', 'latest-time': '2025-04-21T19:53:00Z', 'last-update': '2025-06-16T18:13:27.179Z'}]" +863,MVP,MissHW_Gull-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-22T12:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:15.957Z'}]" +864,MVP,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.926Z'}]" +865,MVP,MissHW_Gull-Tailwater.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.82Z'}]" +866,MVP,MissHW_Gull-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:35:16.819Z'}]" +867,MVP,MissHW_Gull.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-01T13:00:00Z', 'latest-time': '2025-05-23T13:00:00Z', 'last-update': '2025-06-16T18:43:37.56Z'}]" +868,MVP,MissHW_Gull.Area.Inst.1Hour.0.comp,m2,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:38.95Z'}]" +869,MVP,MissHW_Gull.Depth-Ice.Inst.~1Week.0.Raw-NWS-IEM,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-01T13:00:00Z', 'latest-time': '2025-04-01T13:00:00Z', 'last-update': '2025-06-16T18:43:38.744Z'}]" +870,MVP,MissHW_Gull.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:03:03.327Z'}]" +871,MVP,MissHW_Gull.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-10T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:38.917Z'}]" +872,MVP,MissHW_Gull.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:39.042Z'}]" +873,MVP,MissHW_Gull.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,deg,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:39.108Z'}]" +874,MVP,MissHW_Gull.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:03:04.642Z'}]" +875,MVP,MissHW_Gull.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:39.952Z'}]" +876,MVP,MissHW_Gull.Elev.Inst.1Hour.0.merged-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:40.165Z'}]" +877,MVP,MissHW_Gull.Elev.Inst.1Hour.0.rev-NAVD88,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:40.254Z'}]" +878,MVP,MissHW_Gull.Elev.Inst.1Hour.0.rev-NGVD29,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:42.674Z'}]" +879,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:44.00339Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:45.040629Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:43.965972Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:46.319607Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:43.780927Z'}]" +880,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.062Z'}]" +881,MVP,MissHW_Gull.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:43.947Z'}]" +882,MVP,MissHW_Gull.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.085Z'}]" +883,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:45.134Z'}]" +884,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:43:45.258Z'}]" +885,MVP,MissHW_Gull.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:43:45.245Z'}]" +886,MVP,MissHW_Gull.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:45.265Z'}]" +887,MVP,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.295Z'}]" +888,MVP,MissHW_Gull.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:45.411Z'}]" +889,MVP,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.425Z'}]" +890,MVP,MissHW_Gull.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.709609Z'}]" +891,MVP,MissHW_Gull.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.503Z'}]" +892,MVP,MissHW_Gull.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:46.563Z'}]" +893,MVP,MissHW_Gull.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.718Z'}]" +894,MVP,MissHW_Gull.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:47.653Z'}]" +895,MVP,MissHW_Gull.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.667Z'}]" +896,MVP,MissHW_Gull.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.721Z'}]" +897,MVP,MissHW_Gull.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:47.767Z'}]" +898,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:48.983964Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:48.989779Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:49.153614Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:47.953304Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:50.196813Z'}]" +899,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:48.926Z'}]" +900,MVP,MissHW_Gull.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:49.106Z'}]" +901,MVP,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T21:00:00Z', 'latest-time': '2025-06-12T16:00:00Z', 'last-update': '2025-06-16T18:43:48.977Z'}]" +902,MVP,MissHW_Gull.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:49.209Z'}]" +903,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:50.370073Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:51.455235Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:52.623374Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:52.776798Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:50.207866Z'}]" +904,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:50.247Z'}]" +905,MVP,MissHW_Gull.Flow-Sim.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:50.342Z'}]" +906,MVP,MissHW_Gull.Flow.Inst.1Hour.0.comp-gates,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:50.39Z'}]" +907,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:51.870819Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:51.716895Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:51.585669Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:53.934913Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:51.441324Z'}]" +908,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:51.56Z'}]" +909,MVP,MissHW_Gull.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:51.635Z'}]" +910,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:51.888956Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:43:52.646769Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:43:53.971513Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:51.865465Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:51.731686Z'}]" +911,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:51.87Z'}]" +912,MVP,MissHW_Gull.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:43:52.642Z'}]" +913,MVP,MissHW_Gull.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:51.93Z'}]" +914,MVP,MissHW_Gull.Precip-cum.Inst.1Hour.0.rev,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:52.662Z'}]" +915,MVP,MissHW_Gull.Precip-inc.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-05-01T14:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:52.797Z'}]" +916,MVP,MissHW_Gull.Precip-inc.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-04-21T17:00:00Z', 'latest-time': '2025-05-01T18:00:00Z', 'last-update': '2025-06-16T18:43:52.706Z'}]" +917,MVP,MissHW_Gull.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-23T00:00:00Z', 'last-update': '2025-06-16T18:43:52.788Z'}]" +918,MVP,MissHW_Gull.Precip-inc.Total.1Hour.1Hour.comp,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:54.202967Z'}]" +919,MVP,MissHW_Gull.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:53.973266Z'}]" +920,MVP,MissHW_Gull.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,kph,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-22T15:00:00Z', 'last-update': '2025-06-16T18:43:52.945Z'}]" +921,MVP,MissHW_Gull.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:52.959Z'}]" +922,MVP,MissHW_Gull.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:53.904Z'}]" +923,MVP,MissHW_Gull.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-10T17:57:00Z', 'latest-time': '2025-04-21T17:28:00Z', 'last-update': '2025-06-16T18:43:54.05Z'}]" +924,MVP,MissHW_Gull.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-04-22T12:30:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:58.014123Z'}]" +925,MVP,MissHW_Gull.Stage.Inst.1Hour.0.CEMVP-GOES-Raw,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.184Z'}]" +926,MVP,MissHW_Gull.Stage.Inst.1Hour.0.rev,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.098Z'}]" +927,MVP,MissHW_Gull.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:54.144Z'}]" +928,MVP,MissHW_Gull.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:43:54.203Z'}]" +929,MVP,MissHW_Gull.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:55.174Z'}]" +930,MVP,MissHW_Gull.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.218Z'}]" +931,MVP,MissHW_Gull.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.254Z'}]" +932,MVP,MissHW_Gull.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:43:55.283Z'}]" +933,MVP,MissHW_Gull.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:57.90317Z'}]" +934,MVP,MissHW_PineRiver-SlideGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:02.144Z'}]" +935,MVP,MissHW_PineRiver-SlideGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:18.81Z'}]" +936,MVP,MissHW_PineRiver-SlideGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:04.577Z'}]" +937,MVP,MissHW_PineRiver-SlideGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:53.526Z'}]" +938,MVP,MissHW_PineRiver-SlideGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:13.695Z'}]" +939,MVP,MissHW_PineRiver-SlideGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:17.1Z'}]" +940,MVP,MissHW_PineRiver-SlideGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:19.859Z'}]" +941,MVP,MissHW_PineRiver-SlideGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:33.05Z'}]" +942,MVP,MissHW_PineRiver-SlideGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:19.894Z'}]" +943,MVP,MissHW_PineRiver-SlideGate04.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:41.917Z'}]" +944,MVP,MissHW_PineRiver-SlideGate04.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:41.822Z'}]" +945,MVP,MissHW_PineRiver-SlideGate04.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:33:41.58Z'}]" +946,MVP,MissHW_PineRiver-SlideGate05.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:34.495Z'}]" +947,MVP,MissHW_PineRiver-SlideGate05.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T17:56:35.7Z'}]" +948,MVP,MissHW_PineRiver-SlideGate05.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:12:21.335Z'}]" +949,MVP,MissHW_PineRiver-SlideGate06.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:04.855Z'}]" +950,MVP,MissHW_PineRiver-SlideGate06.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:23.511Z'}]" +951,MVP,MissHW_PineRiver-SlideGate06.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:04.782Z'}]" +952,MVP,MissHW_PineRiver-SlideGate07.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:05.945Z'}]" +953,MVP,MissHW_PineRiver-SlideGate07.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:06.138Z'}]" +954,MVP,MissHW_PineRiver-SlideGate07.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:05.862Z'}]" +955,MVP,MissHW_PineRiver-SlideGate08.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.058Z'}]" +956,MVP,MissHW_PineRiver-SlideGate08.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:27.216Z'}]" +957,MVP,MissHW_PineRiver-SlideGate08.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:06.028Z'}]" +958,MVP,MissHW_PineRiver-SlideGate09.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.045Z'}]" +959,MVP,MissHW_PineRiver-SlideGate09.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.345Z'}]" +960,MVP,MissHW_PineRiver-SlideGate09.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:07.213Z'}]" +961,MVP,MissHW_PineRiver-SlideGate10.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:07.441Z'}]" +962,MVP,MissHW_PineRiver-SlideGate10.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:08.645Z'}]" +963,MVP,MissHW_PineRiver-SlideGate10.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:08.25Z'}]" +964,MVP,MissHW_PineRiver-SlideGate11.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:16.165Z'}]" +965,MVP,MissHW_PineRiver-SlideGate11.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:13.563Z'}]" +966,MVP,MissHW_PineRiver-SlideGate11.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:11.06Z'}]" +967,MVP,MissHW_PineRiver-SlideGate12.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:19.682Z'}]" +968,MVP,MissHW_PineRiver-SlideGate12.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:18.638Z'}]" +969,MVP,MissHW_PineRiver-SlideGate12.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:17.581Z'}]" +970,MVP,MissHW_PineRiver-SlideGate13.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:19.759Z'}]" +971,MVP,MissHW_PineRiver-SlideGate13.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:37.718Z'}]" +972,MVP,MissHW_PineRiver-SlideGate13.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:20.993Z'}]" +973,MVP,MissHW_PineRiver-Tailwater.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:21.091Z'}]" +974,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.409Z'}]" +975,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.478Z'}]" +976,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:21.37Z'}]" +977,MVP,MissHW_PineRiver-Tailwater.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.18Z'}]" +978,MVP,MissHW_PineRiver-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:21.268Z'}]" +979,MVP,MissHW_PineRiver-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:22.257Z'}]" +980,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-04-21T22:52:00Z', 'latest-time': '2025-04-21T22:52:00Z', 'last-update': '2025-06-16T18:34:23.467Z'}]" +981,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:24.743Z'}]" +982,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:26.173Z'}]" +983,MVP,MissHW_PineRiver-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:41.532Z'}]" +984,MVP,MissHW_PineRiver.%-Ice.Inst.~1Week.0.Raw-NWS-IEM,%,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-04-07T13:00:00Z', 'latest-time': '2025-04-07T13:00:00Z', 'last-update': '2025-06-16T18:12:39.91Z'}]" +985,MVP,MissHW_PineRiver.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:45.053Z'}]" +986,MVP,MissHW_PineRiver.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:31.558Z'}]" +987,MVP,MissHW_PineRiver.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:12:40.342Z'}]" +988,MVP,MissHW_PineRiver.Dir-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,deg,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:43.944Z'}]" +989,MVP,MissHW_PineRiver.Dir-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,deg,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:47.848044Z'}]" +990,MVP,MissHW_PineRiver.Elev.Ave.1Day.1Day.merged-NGVD29,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:12:42.697Z'}]" +991,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.merged-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:49.247Z'}]" +992,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:12:48.938Z'}]" +993,MVP,MissHW_PineRiver.Elev.Inst.15Minutes.0.rev-NGVD29,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:33.61Z'}]" +994,MVP,MissHW_PineRiver.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:31.576Z'}]" +995,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:35.099117Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:34.912272Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:32.794075Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:32.602972Z'}]" +996,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:32.651Z'}]" +997,MVP,MissHW_PineRiver.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:34.982Z'}]" +998,MVP,MissHW_PineRiver.Elev.Inst.~15Minutes.0.best-NGVD29,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:34.982Z'}]" +999,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:33.79Z'}]" +1000,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:34:33.866Z'}]" +1001,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:34:35.068Z'}]" +1002,MVP,MissHW_PineRiver.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:34:36.151Z'}]" +1003,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:33.921Z'}]" +1004,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:35.31Z'}]" +1005,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:36.386Z'}]" +1006,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.3Days.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:35.166Z'}]" +1007,MVP,MissHW_PineRiver.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:36.33Z'}]" +1008,MVP,MissHW_PineRiver.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.447Z'}]" +1009,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:38.811852Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:37.588394Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:40.092218Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:37.78494Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:36.554876Z'}]" +1010,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.785Z'}]" +1011,MVP,MissHW_PineRiver.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:37.443Z'}]" +1012,MVP,MissHW_PineRiver.Flow-In.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.592Z'}]" +1013,MVP,MissHW_PineRiver.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:40.257Z'}]" +1014,MVP,MissHW_PineRiver.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:39.977Z'}]" +1015,MVP,MissHW_PineRiver.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:38.951Z'}]" +1016,MVP,MissHW_PineRiver.Flow-Out.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:37.675Z'}]" +1017,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:40.387616Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:41.403035Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:40.022999Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:38.731655Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:38.992884Z'}]" +1018,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:41.267Z'}]" +1019,MVP,MissHW_PineRiver.Flow-Out.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:40.301Z'}]" +1020,MVP,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.CEMVP-ProjectEntry,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T16:45:00Z', 'latest-time': '2025-06-05T14:15:00Z', 'last-update': '2025-06-16T18:34:41.437Z'}]" +1021,MVP,MissHW_PineRiver.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:42.732Z'}]" +1022,MVP,MissHW_PineRiver.Flow.Inst.15Minutes.0.comp-gates,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:42.672Z'}]" +1023,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:45.204703Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:45.394003Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:44.038404Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:45.137575Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:43.830993Z'}]" +1024,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:45.271Z'}]" +1025,MVP,MissHW_PineRiver.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:45.191Z'}]" +1026,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:34:46.459963Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:34:44.091152Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:34:44.20912Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:34:45.118593Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:34:43.856872Z'}]" +1027,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:45.302Z'}]" +1028,MVP,MissHW_PineRiver.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:34:45.18Z'}]" +1029,MVP,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.398Z'}]" +1030,MVP,MissHW_PineRiver.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:47.908Z'}]" +1031,MVP,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.CEMVP-GOES-Raw,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:47.587Z'}]" +1032,MVP,MissHW_PineRiver.Precip-cum.Inst.1Hour.0.rev,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.478Z'}]" +1033,MVP,MissHW_PineRiver.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.702Z'}]" +1034,MVP,MissHW_PineRiver.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.381Z'}]" +1035,MVP,MissHW_PineRiver.Precip-inc.Total.1Hour.1Hour.comp,mm,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:46.687Z'}]" +1036,MVP,MissHW_PineRiver.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:50.287Z'}]" +1037,MVP,MissHW_PineRiver.Speed-Wind.Inst.15Minutes.0.CEMVP-GOES-Raw,kph,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:50.317Z'}]" +1038,MVP,MissHW_PineRiver.Speed-Wind.Inst.1Hour.0.CEMVP-GOES-Raw,kph,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:51.422Z'}]" +1039,MVP,MissHW_PineRiver.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:48.862Z'}]" +1040,MVP,MissHW_PineRiver.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:51.777Z'}]" +1041,MVP,MissHW_PineRiver.Stage.Inst.0.0.Raw-CEMVP,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-04-21T23:00:00Z', 'latest-time': '2025-04-21T23:00:00Z', 'last-update': '2025-06-16T18:34:51.802Z'}]" +1042,MVP,MissHW_PineRiver.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:53.116Z'}]" +1043,MVP,MissHW_PineRiver.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:53.976Z'}]" +1044,MVP,MissHW_PineRiver.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:54.346Z'}]" +1045,MVP,MissHW_PineRiver.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:52.803Z'}]" +1046,MVP,MissHW_PineRiver.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:54.099Z'}]" +1047,MVP,MissHW_PineRiver.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:54.111Z'}]" +1048,MVP,MissHW_PineRiver.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:55.434Z'}]" +1049,MVP,MissHW_PineRiver.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:34:55.528Z'}]" +1050,MVP,MissHW_PineRiver.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:57.01Z'}]" +1051,MVP,Muscoda.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:07.425Z'}]" +1052,MVP,Muscoda.Elev.Inst.~15Minutes.0.Raw-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:04.714Z'}]" +1053,MVP,Muscoda.Elev.Inst.~15Minutes.0.rev-USGS-NAVD88,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:06.422Z'}]" +1054,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:08.700079Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:07.583556Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:07.426816Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:06.421326Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:05.165804Z'}]" +1055,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:08.52Z'}]" +1056,MVP,Muscoda.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:08.887Z'}]" +1057,MVP,Muscoda.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-04T05:00:00Z', 'latest-time': '2025-05-21T05:00:00Z', 'last-update': '2025-06-16T18:42:08.967Z'}]" +1058,MVP,Muscoda.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:10.042Z'}]" +1059,MVP,Muscoda.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:10.137Z'}]" +1060,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:11.452838Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:13.738115Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:13.597948Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:12.329473Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:11.200671Z'}]" +1061,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:12.285Z'}]" +1062,MVP,Muscoda.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:12.419Z'}]" +1063,MVP,Muscoda.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-02T21:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:12.529Z'}]" +1064,MVP,Muscoda.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-02T21:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:13.84Z'}]" +1065,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:42:14.843459Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:14.853578Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:13.93776Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:13.933483Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:13.694114Z'}]" +1066,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:15.019Z'}]" +1067,MVP,Muscoda.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:42:14.813Z'}]" +1068,MVP,Muscoda.Precip-cum.Inst.15Minutes.0.CEMVP-GOES-Raw,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.091Z'}]" +1069,MVP,Muscoda.Precip-cum.Inst.15Minutes.0.rev,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.235Z'}]" +1070,MVP,Muscoda.Precip-inc.Total.15Minutes.15Minutes.comp,mm,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.09Z'}]" +1071,MVP,Muscoda.Precip-inc.Total.1Day.1Day.comp,mm,1Day,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:15.146Z'}]" +1072,MVP,Muscoda.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.542Z'}]" +1073,MVP,Muscoda.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:16.438Z'}]" +1074,MVP,Muscoda.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:18.798Z'}]" +1075,MVP,Muscoda.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:21.339Z'}]" +1076,MVP,Muscoda.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-22T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:20.025Z'}]" +1077,MVP,Muscoda.Temp-Water.Inst.15Minutes.0.CEMVP-GOES-Raw,C,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:22.555Z'}]" +1078,MVP,Muscoda.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:21.567Z'}]" +1079,MVP,NWUM5.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:30.081Z'}]" +1080,MVP,NWUM5.Elev.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:30.563Z'}]" +1081,MVP,NWUM5.Elev.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:32.684Z'}]" +1082,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:33.911783Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:32.88008Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:31.798013Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:33.942497Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:31.639567Z'}]" +1083,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:33.944Z'}]" +1084,MVP,NWUM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:34.165Z'}]" +1085,MVP,NWUM5.Flow.Ave.1Day.1Day.rev-USGS,cms,1Day,300,US/Central,"[{'earliest-time': '2025-03-13T05:00:00Z', 'latest-time': '2025-05-10T05:00:00Z', 'last-update': '2025-06-16T18:42:34.345Z'}]" +1086,MVP,NWUM5.Flow.Inst.0.0.Raw-USGS,cms,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-12T18:22:00Z', 'latest-time': '2025-05-06T17:29:00Z', 'last-update': '2025-06-16T18:42:34.263Z'}]" +1087,MVP,NWUM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:35.472Z'}]" +1088,MVP,NWUM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:35.564Z'}]" +1089,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:38.075857Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:37.76879Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:37.694397Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:38.982635Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:36.703753Z'}]" +1090,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:39.002Z'}]" +1091,MVP,NWUM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:38.941Z'}]" +1092,MVP,NWUM5.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T22:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:40.232Z'}]" +1093,MVP,NWUM5.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T22:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:39.369Z'}]" +1094,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:42:41.778234Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:42:40.613757Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:42:40.588447Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:42:40.567401Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:42:40.372884Z'}]" +1095,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:41.655Z'}]" +1096,MVP,NWUM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:41.613Z'}]" +1097,MVP,NWUM5.Stage.Inst.0.0.Raw-USGS,m,0,-2147483648,US/Central,"[{'earliest-time': '2025-03-12T18:22:00Z', 'latest-time': '2025-05-06T17:29:00Z', 'last-update': '2025-06-16T18:42:41.629Z'}]" +1098,MVP,NWUM5.Stage.Inst.15Minutes.0.OTHER-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.827Z'}]" +1099,MVP,NWUM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.881Z'}]" +1100,MVP,NWUM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:44.166Z'}]" +1101,MVP,NWUM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.976Z'}]" +1102,MVP,NWUM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-05-11T06:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:42.866Z'}]" +1103,MVP,NWUM5.Volt.Inst.1Hour.0.OTHER-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-20T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:42:43.193Z'}]" +1104,MVP,Rafferty_Dam-Tailwater.Flow.Inst.5Minutes.0.Raw-EnvCan,cms,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:19.520532Z'}]" +1105,MVP,Rafferty_Dam-Tailwater.Stage.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:08.058Z'}]" +1106,MVP,Rafferty_Dam.Elev.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:20.601075Z'}]" +1107,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS,m,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:59.011086Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:59.121663Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:56.829077Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:57.749501Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:56.687811Z'}]" +1108,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:56.593Z'}]" +1109,MVP,Rafferty_Dam.Elev.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:56.835Z'}]" +1110,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.578845Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:00.436975Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:00.696008Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:00.304716Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:57.909433Z'}]" +1111,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:58.03Z'}]" +1112,MVP,Rafferty_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:58.028Z'}]" +1113,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:43:59.211311Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:01.565275Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:59.23332Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:00.29726Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:43:59.10088Z'}]" +1114,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:43:59.361Z'}]" +1115,MVP,Rafferty_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:43:59.234Z'}]" +1116,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.340506Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:43:59.69923Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:43:59.673483Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:43:59.560864Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:01.568751Z'}]" +1117,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:00.294Z'}]" +1118,MVP,Rafferty_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:44:00.438Z'}]" +1119,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:44:00.709286Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:44:01.563956Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:44:03.024022Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:44:02.905741Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:44:00.596076Z'}]" +1120,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:00.695Z'}]" +1121,MVP,Rafferty_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:44:00.712Z'}]" +1122,MVP,Rafferty_Dam.Stage.Inst.5Minutes.0.Raw-EnvCan,m,5Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:44:29.820115Z'}]" +1123,MVP,TraverseWR_Dam-Lake.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:20.879Z'}]" +1124,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:20.933Z'}]" +1125,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.042Z'}]" +1126,MVP,TraverseWR_Dam-Lake.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.066Z'}]" +1127,MVP,TraverseWR_Dam-Lake.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:21.935Z'}]" +1128,MVP,TraverseWR_Dam-Lake.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:22.124Z'}]" +1129,MVP,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-13T02:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.326Z'}]" +1130,MVP,TraverseWR_Dam-Lake.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:24.352Z'}]" +1131,MVP,TraverseWR_Dam-Lake.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:25.679Z'}]" +1132,MVP,TraverseWR_Dam-Lake.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:23.487Z'}]" +1133,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-15T19:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:24.604Z'}]" +1134,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:24.653Z'}]" +1135,MVP,TraverseWR_Dam-MainLake.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:32:25.504Z'}]" +1136,MVP,TraverseWR_Dam-Tailwater.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:42.41Z'}]" +1137,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:44.675Z'}]" +1138,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:44.991Z'}]" +1139,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.38Z'}]" +1140,MVP,TraverseWR_Dam-Tailwater.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:46.282Z'}]" +1141,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.385Z'}]" +1142,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:47.646Z'}]" +1143,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.Raw-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-31T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:46.182Z'}]" +1144,MVP,TraverseWR_Dam-Tailwater.Flow.Inst.~15Minutes.0.rev-USGS,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-31T00:45:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:48.801Z'}]" +1145,MVP,TraverseWR_Dam-Tailwater.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:47.299Z'}]" +1146,MVP,TraverseWR_Dam-Tailwater.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:50.102Z'}]" +1147,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:51.441Z'}]" +1148,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.877Z'}]" +1149,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.729Z'}]" +1150,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:56.261Z'}]" +1151,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:00:56.348Z'}]" +1152,MVP,TraverseWR_Dam-Tailwater.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:39.877Z'}]" +1153,MVP,TraverseWR_Dam-TainterGate01.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:43.332Z'}]" +1154,MVP,TraverseWR_Dam-TainterGate01.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:46.72Z'}]" +1155,MVP,TraverseWR_Dam-TainterGate01.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:33:43.144Z'}]" +1156,MVP,TraverseWR_Dam-TainterGate02.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:25.948Z'}]" +1157,MVP,TraverseWR_Dam-TainterGate02.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:28.249Z'}]" +1158,MVP,TraverseWR_Dam-TainterGate02.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:40:27.213Z'}]" +1159,MVP,TraverseWR_Dam-TainterGate03.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:34:06.104124Z'}]" +1160,MVP,TraverseWR_Dam-TainterGate03.Opening.Inst.15Minutes.0.CEMVP-ProjectEntry,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:58.45Z'}]" +1161,MVP,TraverseWR_Dam-TainterGate03.Opening.Inst.~15Minutes.0.CEMVP-ProjectEntry,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-07T09:30:00Z', 'latest-time': '2025-06-04T20:45:00Z', 'last-update': '2025-06-16T18:12:17.431Z'}]" +1162,MVP,TraverseWR_Dam.Area.Inst.15Minutes.0.comp,m2,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:40.781Z'}]" +1163,MVP,TraverseWR_Dam.Depth-Inc-Snow.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:52.491Z'}]" +1164,MVP,TraverseWR_Dam.Depth-SWE.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-22T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:53.586Z'}]" +1165,MVP,TraverseWR_Dam.Depth-Snow.Total.~1Week.1Month.Raw-NWS-ACIS,mm,~1Week,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:53.782Z'}]" +1166,MVP,TraverseWR_Dam.Elev.Ave.1Day.1Day.merged-MSL1912,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:55.217Z'}]" +1167,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.merged-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.491Z'}]" +1168,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-MSL1912,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.193Z'}]" +1169,MVP,TraverseWR_Dam.Elev.Inst.15Minutes.0.rev-NAVD88,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:56.302Z'}]" +1170,MVP,TraverseWR_Dam.Elev.Inst.1Hour.0.Fcst-CEMVP,m,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:18:41.1Z'}]" +1171,MVP,TraverseWR_Dam.Elev.Inst.~15Minutes.0.best-MSL1912,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:01.504Z'}]" +1172,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:06.555468Z'}]" +1173,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.1Day.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:59.893Z'}]" +1174,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:57.316Z'}]" +1175,MVP,TraverseWR_Dam.Flow-In.Ave.15Minutes.6Hours.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:05.313633Z'}]" +1176,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:40:56.413Z'}]" +1177,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Month.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-05-28T12:00:00Z', 'last-update': '2025-06-16T18:41:01.403Z'}]" +1178,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.1Week.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-04T12:00:00Z', 'latest-time': '2025-06-09T12:00:00Z', 'last-update': '2025-06-16T18:41:00.246Z'}]" +1179,MVP,TraverseWR_Dam.Flow-In.Ave.1Day.3Days.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:40:58.988Z'}]" +1180,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:40:59.004Z'}]" +1181,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.1Day.comp-noNeg,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:02.823739Z'}]" +1182,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.3Days.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-06T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:03.998Z'}]" +1183,MVP,TraverseWR_Dam.Flow-In.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:02.898Z'}]" +1184,MVP,TraverseWR_Dam.Flow-In.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:06.493Z'}]" +1185,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:07.475901Z'}]" +1186,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:01:06.463Z'}]" +1187,MVP,TraverseWR_Dam.Flow-In.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:18:52.473Z'}]" +1188,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:10.272837Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:10.073007Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:08.909855Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:11.336012Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:11.408089Z'}]" +1189,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.01Z'}]" +1190,MVP,TraverseWR_Dam.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:10.12Z'}]" +1191,MVP,TraverseWR_Dam.Flow-Out.Ave.1Day.1Day.comp,cms,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:10.216Z'}]" +1192,MVP,TraverseWR_Dam.Flow-Out.Ave.6Hours.6Hours.comp,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:11.509Z'}]" +1193,MVP,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-07T00:15:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.323Z'}]" +1194,MVP,TraverseWR_Dam.Flow-Out.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.395Z'}]" +1195,MVP,TraverseWR_Dam.Flow-Out.Inst.1Hour.0.rev,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.4Z'}]" +1196,MVP,TraverseWR_Dam.Flow-Out.Inst.~15Minutes.0.best,cms,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:11.298Z'}]" +1197,MVP,TraverseWR_Dam.Flow.Inst.1Hour.0.Fcst-CEMVP,cms,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:10.44Z'}]" +1198,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:12.719742Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:13.846029Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:15.170814Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:13.995Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:12.849292Z'}]" +1199,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:12.57Z'}]" +1200,MVP,TraverseWR_Dam.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:12.913Z'}]" +1201,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:15.52092Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:17.897955Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:18.974952Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:17.706871Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:15.320425Z'}]" +1202,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:15.257Z'}]" +1203,MVP,TraverseWR_Dam.Precip-Rain.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:15.206Z'}]" +1204,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-25T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-06T01:11:00Z', 'last-update': '2025-06-16T18:41:17.700779Z'}, {'earliest-time': '2025-05-26T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-07T01:11:00Z', 'last-update': '2025-06-16T18:41:16.61681Z'}, {'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:41:17.947231Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:41:16.440211Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:41:18.969499Z'}]" +1205,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.896Z'}]" +1206,MVP,TraverseWR_Dam.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-03-18T12:00:00Z', 'last-update': '2025-06-16T18:41:18.938Z'}]" +1207,MVP,TraverseWR_Dam.Precip-inc.Total.~1Day.1Day.CEMVP-ProjectEntry,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T14:00:00Z', 'latest-time': '2025-06-11T13:00:00Z', 'last-update': '2025-06-16T18:41:16.454Z'}]" +1208,MVP,TraverseWR_Dam.Precip.Total.~1Day.1Day.Raw-NWS-ACIS,mm,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:16.619Z'}]" +1209,MVP,TraverseWR_Dam.Stage.Ave.1Day.1Day.comp,m,1Day,720,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:16.747Z'}]" +1210,MVP,TraverseWR_Dam.Stage.Ave.6Hours.6Hours.comp,m,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.677Z'}]" +1211,MVP,TraverseWR_Dam.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:17.939Z'}]" +1212,MVP,TraverseWR_Dam.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:19.226Z'}]" +1213,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:19.122Z'}]" +1214,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.best,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.544Z'}]" +1215,MVP,TraverseWR_Dam.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.346Z'}]" +1216,MVP,TraverseWR_Dam.Stor.Ave.15Minutes.2Hours.comp,m3,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.436Z'}]" +1217,MVP,TraverseWR_Dam.Stor.Ave.1Day.1Day.comp,m3,1Day,720,US/Central,"[{'earliest-time': '2025-03-07T12:00:00Z', 'latest-time': '2025-06-12T12:00:00Z', 'last-update': '2025-06-16T18:41:19.194Z'}]" +1218,MVP,TraverseWR_Dam.Stor.Ave.6Hours.6Hours.comp,m3,6Hours,0,US/Central,"[{'earliest-time': '2025-03-07T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:20.18Z'}]" +1219,MVP,TraverseWR_Dam.Temp-Air.Inst.~1Day.0.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-03T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:20.673Z'}]" +1220,MVP,TraverseWR_Dam.Temp-Air.Max.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:21.629Z'}]" +1221,MVP,TraverseWR_Dam.Temp-Air.Min.~1Day.1Day.Raw-NWS-ACIS,C,~1Day,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T12:00:00Z', 'latest-time': '2025-06-11T12:00:00Z', 'last-update': '2025-06-16T18:41:22.836Z'}]" +1222,MVP,TraverseWR_Dam.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:41:25.332471Z'}]" +1223,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:34.180276Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:33.007808Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:32.704893Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:34.24004Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:32.991601Z'}]" +1224,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:41.138Z'}]" +1225,MVP,ZUMM5.Flow-Local.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:34.262Z'}]" +1226,MVP,ZUMM5.Flow.Inst.15Minutes.0.comp,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:36.509Z'}]" +1227,MVP,ZUMM5.Flow.Inst.15Minutes.0.rev,cms,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:21:42.734Z'}]" +1228,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:37.962076Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:36.733914Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:39.041481Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:37.823873Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:36.50295Z'}]" +1229,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-Auto,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:36.888Z'}]" +1230,MVP,ZUMM5.Flow.Inst.6Hours.0.Fcst-NCRFC-CHIPS-CRF,cms,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:36.807Z'}]" +1231,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-05-27T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-08T01:11:00Z', 'last-update': '2025-06-16T18:33:39.257429Z'}, {'earliest-time': '2025-05-28T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-09T01:11:00Z', 'last-update': '2025-06-16T18:33:39.100908Z'}, {'earliest-time': '2025-05-30T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-10T01:11:00Z', 'last-update': '2025-06-16T18:33:40.366749Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-11T01:11:00Z', 'last-update': '2025-06-16T18:33:40.609429Z'}, {'earliest-time': '2025-05-29T12:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'version-time': '2025-06-12T01:11:00Z', 'last-update': '2025-06-16T18:33:38.101221Z'}]" +1232,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-Auto,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.203Z'}]" +1233,MVP,ZUMM5.Precip-RainAndMelt.Total.6Hours.6Hours.Fcst-NCRFC-CHIPS-CRF,mm,6Hours,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-04-01T12:00:00Z', 'last-update': '2025-06-16T18:33:39.133Z'}]" +1234,MVP,ZUMM5.Stage.Inst.15Minutes.0.CEMVP-GOES-Raw,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.409Z'}]" +1235,MVP,ZUMM5.Stage.Inst.15Minutes.0.corrected-comp,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:39.459Z'}]" +1236,MVP,ZUMM5.Stage.Inst.15Minutes.0.rev,m,15Minutes,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.41Z'}]" +1237,MVP,ZUMM5.Stage.Inst.~15Minutes.0.Raw-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.53Z'}]" +1238,MVP,ZUMM5.Stage.Inst.~15Minutes.0.rev-USGS,m,~15Minutes,-2147483648,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:40.668Z'}]" +1239,MVP,ZUMM5.Volt.Inst.1Hour.0.CEMVP-GOES-Raw,volt,1Hour,0,US/Central,"[{'earliest-time': '2025-03-01T00:00:00Z', 'latest-time': '2025-06-13T00:00:00Z', 'last-update': '2025-06-16T18:33:42.799Z'}]"